A modular command-line toolkit for processing PDF documents. Run main.py to access all features through a central menu.
- Project Structure
- Requirements
- Installation
- How to Run
- Modules
- Adding a New Module
- Troubleshooting
- Notes
project/
├── main.py ← entry point and main menu
├── config.py ← centralized settings for all modules
└── modules/
├── __init__.py ← marks modules/ as a Python package (intentionally empty)
├── document_manager.py ← scans a drive and copies matching PDFs
├── brand_reader.py ← extracts brand name fields into Excel
├── batch_printer.py ← batch prints PDFs to a physical printer
└── config_editor.py ← interactive configuration settings editor
Module files use snake_case names, matching the imports in
main.py(modules.document_manager,modules.brand_reader,modules.batch_printer,modules.config_editor).
Python 3.10 or higher — required for the str | None type hint syntax used throughout the modules.
To check your version:
python --version
Install all required packages in one command:
pip install pypdf pdfplumber openpyxl pywin32
| Package | Required by | Purpose |
|---|---|---|
| pypdf | document_manager, brand_reader | Primary PDF text extraction |
| pdfplumber | document_manager, brand_reader | Fallback extraction for complex layouts |
| openpyxl | brand_reader | Writing formatted Excel reports |
| pywin32 | batch_printer | Windows printer spooler access |
Only required if your PDFs are scanned images rather than digitally created documents. The toolkit functions without OCR — it simply skips the OCR step.
pip install pytesseract pdf2image
You must also install the following external binaries:
Tesseract OCR
Download: https://github.com/UB-Mannheim/tesseract/wiki
Default path expected: C:\Program Files\Tesseract-OCR\tesseract.exe
Update TESSERACT_PATH in config.py if installed elsewhere.
Poppler
Download: https://github.com/oschwartz10612/poppler-windows/releases
Default path expected: C:\poppler-26.02.0\Library\bin
Update POPPLER_PATH in config.py if installed elsewhere.
The Batch Print module uses Ghostscript to send PDFs to the printer, forcing a consistent paper size regardless of the source PDF's page size.
Download: https://www.ghostscript.com/releases/gsdnld.html (Windows 64-bit installer)
Default path expected: C:\Program Files\gs\<version>\bin\gswin64c.exe
Update GHOSTSCRIPT_PATH in config.py to match your installed version —
the version folder name (e.g. gs10.07.1) changes with each release, so
this must be updated after every Ghostscript upgrade.
Licensing note: Ghostscript is distributed under AGPL or a commercial license. Calling the unmodified
gswin64c.exebinary via subprocess for internal batch printing is generally considered low-risk under AGPL (no modification or network-service redistribution involved), but if your organization has a formal software compliance process, confirm this usage against it before deploying.
-
Clone or download this repository into a local folder.
-
Install Python dependencies:
pip install pypdf pdfplumber openpyxl pywin32 -
If OCR is needed:
pip install pytesseract pdf2imageThen install Tesseract and Poppler binaries (see links above) and update
TESSERACT_PATH/POPPLER_PATHinconfig.py(or use the interactive Configuration Editor inmain.pyto set them). -
For batch printing, install Ghostscript: Download from https://www.ghostscript.com/releases/gsdnld.html and update
GHOSTSCRIPT_PATHinconfig.pyto match the installedgswin64c.exepath (or set it via the Configuration Editor). -
Set
PRINTER_NAMEinconfig.pyto the exact printer name as registered in Windows (Settings → Bluetooth & devices → Printers & scanners). This can also be adjusted via the Configuration Editor. This is checked automatically at the start of every batch print run — see Batch Print below. -
Run the toolkit:
python main.py
From inside the project/ folder:
python main.py
You will be presented with the following menu:
=======================================================
PDF Processing Toolkit
=======================================================
1. Scan drive and copy matching PDFs (pdf_scanner)
2. Extract brand names to Excel (brand_reader)
3. Batch print PDFs to printer (batch_print)
4. Configure Toolkit Settings (config_editor)
0. Exit
=======================================================
Select an option by typing the number and pressing Enter. After each operation completes, press Enter to return to the menu.
If a module's dependencies are missing, its menu entry is marked
⚠ missing deps and selecting it prints the required pip install command
instead of running.
Recursively walks a drive or folder, identifies PDFs that match a configurable keyword and regex combination, and copies them to a destination folder.
A PDF qualifies only when both of the following conditions are true:
- At least one keyword from
KEYWORDSis found anywhere in the extracted text (case-insensitive), AND - At least
MATCH_THRESHOLDof theMATCHERSregex patterns also match
Pages are read one at a time and scanning stops the moment both conditions are satisfied — the remaining pages are never read.
Certificate of Good Manufacturing Practice
Certificate of Product Registration
Certificate of Listing of Identical Drug Product
Brand Name:
Registration Number:
FDA Registration No.:
Valid Until <date>
Manufacturer:
Importer / Distributor:
To add keywords, append to the KEYWORDS list in document_manager.py.
To make matching stricter, raise MATCH_THRESHOLD in config.py.
pypdf → pdfplumber → OCR (Tesseract)
- pypdf — fastest, works on standard digitally created PDFs
- pdfplumber — slower, handles complex layouts, tables, and multi-column text
- OCR — slowest, used only when extracted text is below
TEXT_THRESHOLDcharacters
If a method returns sufficient text and the match conditions are met, the remaining methods are never attempted.
- Walks all subdirectories recursively regardless of nesting depth
- Walk and processing run concurrently — the first file starts processing while the walker is still discovering new directories
- The destination folder is automatically excluded from the walk to prevent re-processing already-copied files
- Current directory being scanned is displayed on a single overwriting console line
Folders always skipped:
.* (any hidden folder) $* (system folders) ~* (temp folders)
Windows Program Files Program Files (x86)
ProgramData System Volume Information winnt
Files are SHA-256 hashed before copying:
- Files under 512 KB: full file hashed
- Files over 512 KB: first + last 256 KB hashed (for speed)
If a matching file with identical content has already been copied in the current run (regardless of filename or location), it is skipped and logged under DUPLICATES in the output log.
All settings are in config.py:
| Setting | Default | Description |
|---|---|---|
SEARCH_ROOT |
"" |
Drive or folder to scan. Empty = prompted at runtime |
DEST_FOLDER |
"" |
Destination for copied files. Empty = prompted |
MAX_WORKERS |
4 | Parallel worker processes (bypasses GIL) |
MAX_PAGES |
5 | Maximum pages to scan per PDF |
TEXT_THRESHOLD |
50 | Minimum characters before trying next extractor |
FILE_TIMEOUT |
30 | Seconds before abandoning a single file |
MIN_FILE_SIZE |
1024 | Skip files smaller than this in bytes |
MAX_FILE_SIZE |
0 | Skip files larger than this in bytes (0 = no limit) |
MOVE_FILES |
False | True = move files, False = copy files |
SKIP_DUPLICATES |
True | Skip files with identical content |
SKIP_HIDDEN |
True | Skip hidden and system folders |
MATCH_THRESHOLD |
2 | Minimum regex pattern hits required |
OCR_DPI |
150 | DPI for OCR image rendering |
TESSERACT_PATH |
— | Full path to tesseract.exe |
POPPLER_PATH |
— | Full path to Poppler bin folder |
Important:
MOVE_FILESdefaults toFalse. Always verify results in copy mode before switching toTrue. Moving files is irreversible.
A scan_results.txt log is written to the destination folder containing:
- Full source → destination path for every copied file
- All skipped files (no match)
- All duplicate files (same content, skipped)
- All errors with error messages
- Summary counts at the bottom
Scans all PDFs in a single folder and extracts structured regulatory fields into a formatted Excel report.
| Field | Pattern matched |
|---|---|
| Brand Name | Brand Name: |
| Registration No. | Registration Number: / FDA Registration No.: |
| Valid Until | valid until <date> |
| Manufacturer | Manufacturer: / Manufacturer Name and Address: |
| Trader | Trader: |
| Importer | Importer: / Importer / Distributor: |
| Distributor | Distributor: |
Same as the PDF Scanner: pypdf → pdfplumber → OCR (Tesseract).
On the first run, every PDF is processed and its results are cached. On subsequent runs, unchanged files are served from the cache — only new or modified files are re-extracted.
- Each file is fingerprinted by its size and modification time
(
size:mtime_ns, hashed with SHA-256) - The cache is stored as
.brand_cache.jsonin the scanned folder - If an unchanged file previously produced an error, the cached error is replayed instead of re-attempting extraction
- The cache is updated automatically after every run
- If the cache file is missing or corrupt, it is simply recreated — no user action is needed
This makes repeated runs over the same folder dramatically faster, since expensive PDF parsing and OCR are skipped for files that have not changed.
All settings are in config.py:
| Setting | Default | Description |
|---|---|---|
MAX_WORKERS |
4 | Parallel worker threads |
MAX_PAGES |
5 | Maximum pages to scan per PDF |
OCR_DPI_HIGH |
300 | DPI for OCR rendering |
TEXT_THRESHOLD |
50 | Minimum characters before trying next extractor |
TESSERACT_PATH |
— | Full path to tesseract.exe |
POPPLER_PATH |
— | Full path to Poppler bin folder |
BRAND_CACHE_FILE |
.brand_cache.json |
Cache filename for result reuse |
A brand_results.xlsx file is written to the scanned folder. If the file already exists it is saved as brand_results(1).xlsx, brand_results(2).xlsx, and so on — existing files are never overwritten.
The Excel report is divided into three labeled sections:
| Section | Contents |
|---|---|
| ✔ FOUND | Files where Brand Name was successfully extracted |
| ✘ NOT FOUND | Files processed but Brand Name was not found |
| ⚠ ERRORS | Files that could not be read or caused exceptions |
A summary row at the bottom shows total counts for each section.
Sends a configurable page range of all PDFs in a folder to a physical printer in natural sort order, with a live dashboard showing real-time print queue state.
Windows only. This module requires
pywin32and the Windows print spooler. It will not run on macOS or Linux.
- On startup,
_validate_environment()confirmsGHOSTSCRIPT_PATHpoints to an existing file andPRINTER_NAMEmatches a printer registered in Windows. If either check fails, the run aborts immediately — on a printer-name mismatch, the actual list of registered printer names is printed soconfig.pycan be corrected. - PDFs are sorted in natural order (
cert2.pdfbeforecert10.pdf) - Before sending each file, the module checks the spooler — if
MAX_ACTIVE_JOBSis already in the queue, it waits - The configured page range of each file is sent via Ghostscript
(
mswinpr2device) in silent mode. The job is forced to Letter paper and the page content is scaled to fit (-sPAPERSIZE=letter,-dFIXEDMEDIA,-dPDFFitPage), which resolves A4/Letter paper-size mismatches when source PDFs are A4-sized but the printer tray is loaded with Letter - The spooler job ID is captured by comparing job lists before and after sending — if Ghostscript completes the job too quickly for the spooler to register it, the file is marked complete immediately rather than left "in progress" indefinitely
- Completed jobs are detected when their ID disappears from the active spooler
- After the last file is sent, a drain loop waits up to
DRAIN_TIMEOUTseconds for all remaining jobs to clear
All settings are in config.py:
| Setting | Default | Description |
|---|---|---|
PRINTER_NAME |
DocuPrint M455 df |
Exact printer name as registered in Windows — verified automatically at the start of every run |
GHOSTSCRIPT_PATH |
— | Full path to gswin64c.exe (e.g. C:\Program Files\gs\gs10.07.1\bin\gswin64c.exe) |
MAX_ACTIVE_JOBS |
2 | Maximum concurrent spooler jobs before waiting |
PRINT_FIRST_PAGE |
1 | First page of each PDF to print (1-based) |
PRINT_LAST_PAGE |
1 | Last page of each PDF to print (0 = last page of document) |
To find the exact printer name: open Settings → Bluetooth & devices →
Printers & scanners, click the printer, and copy the name exactly as
displayed. If PRINTER_NAME doesn't match, the next run will print the full
list of registered names so you can correct it.
Page range tips:
PRINT_FIRST_PAGE = 1,PRINT_LAST_PAGE = 1(default) — prints only page 1, intended for cover-page/letterhead printingPRINT_FIRST_PAGE = 1,PRINT_LAST_PAGE = 0— prints the full documentPRINT_FIRST_PAGE = 2,PRINT_LAST_PAGE = 5— prints pages 2 through 5
You can adjust the page range via the Configuration Editor under
Batch Printer Settings, or directly in config.py.
A print_history.txt log is written to the PDF source folder on completion, listing all printed files in order. Files that failed to send are tagged with [FAILED].
An interactive, menu-driven CLI configuration editor. It allows you to view, validate, and customize all toolkit settings dynamically without manually editing Python files.
- Interactive Menus: Settings are organized into 5 logical categories (Global/Concurrency, PDF Scanner, Brand Reader, Batch Printer, and OCR Engine).
- Automatic Validation: Validates user inputs on the fly (e.g., checks if specified paths exist, verifies non-negative integers, and converts boolean inputs).
- Persistent Overrides: Saves configuration overrides to
config_local.jsonin the project root. This file is automatically loaded byconfig.pyso customized settings persist across runs. - In-Memory Session Sync: Changed settings are synced in-memory dynamically in real-time to already-loaded pipeline modules during the active toolkit session.
| Category | Description | Key Settings Managed |
|---|---|---|
| 1. Global & Concurrency Settings | General execution limits. | MAX_WORKERS, MAX_PAGES |
| 2. PDF Scanner Settings | PDF scanning paths and thresholds. | SEARCH_ROOT, DEST_FOLDER, SCAN_LOG_FILE, MATCH_THRESHOLD, MOVE_FILES, SKIP_DUPLICATES, SKIP_HIDDEN, MIN_FILE_SIZE, MAX_FILE_SIZE, FILE_TIMEOUT |
| 3. Brand Reader Settings | Brand reader extraction reporting. | BRAND_LOG_FILE |
| 4. Batch Printer Settings | Printer target and page-range configuration. | PRINTER_NAME, MAX_ACTIVE_JOBS, GHOSTSCRIPT_PATH, PRINT_FIRST_PAGE, PRINT_LAST_PAGE |
| 5. OCR Engine Settings | External binaries and OCR DPI. | TESSERACT_PATH, POPPLER_PATH, OCR_DPI, OCR_DPI_HIGH, TEXT_THRESHOLD |
-
Create
modules/your_module.pywith arun()function:def run(folder_path: str) -> None: # your logic here
-
In
main.py, import it with_try_import(returnsNoneif dependencies are missing, rather than crashing):your_module = _try_import("modules.your_module")
-
Add an entry to
MENU_ENTRIES:MENU_ENTRIES = [ ... ( "Your feature description (your_module)", your_module, "launch_your_module", ), ]
-
Add a launcher function, including a missing-dependency check:
def launch_your_module(): if your_module is None: _missing_deps_notice("yourModule", "required-package") return print("\n── Your Module ──────────────────────────────────────") folder = prompt_path("Enter folder path", must_exist=True) your_module.run(folder)
Menu numbering updates automatically based on MENU_ENTRIES's order and length.
Menu entry shows ⚠ missing deps
The corresponding module's dependencies aren't installed. Selecting the entry prints the required pip install command — run it and restart main.py.
ModuleNotFoundError: No module named 'win32print'
Run pip install pywin32. This is required for Batch Print only.
ModuleNotFoundError: No module named 'pytesseract'
OCR is optional. If not installed, the toolkit falls back to text-only extraction. Install with pip install pytesseract pdf2image only if your PDFs are scanned images.
PDF scanner finds no matches
- Confirm your PDFs contain one of the three certificate title keywords
- Lower
MATCH_THRESHOLDto1inconfig.pytemporarily to test keyword-only matching - If PDFs are scanned images, ensure OCR is installed and
OCR_DPIis at least 200
Brand reader returns empty fields
- The field labels in the PDF must match the regex patterns (e.g.
Brand Name:,Manufacturer:) - Check if the PDF is image-based — if so, OCR must be installed
OCR_DPI_HIGH(default 300) is used for brand extraction; raise it for low-quality scans
Brand reader processed nothing on a re-run
This is expected — unchanged files are served from the cache (.brand_cache.json in the scanned folder). Only new or modified files are re-processed. Delete the cache file to force a full re-extraction.
❌ Ghostscript not found at: ...
Update GHOSTSCRIPT_PATH in config.py — the version folder name (e.g. gs10.07.1) changes with each Ghostscript release, so this needs updating after upgrades.
❌ Printer 'X' not found in Windows printer list.
PRINTER_NAME in config.py doesn't exactly match a printer registered in Windows. The error prints the full list of available names — copy the exact string (including any manufacturer prefix) into PRINTER_NAME.
A small window showing a percentage briefly appears for each printed file This is the printer driver's own status display during the Ghostscript job. It closes automatically once the job is sent and does not block the batch loop.
Batch print sends jobs but dashboard shows no completion
- Verify
PRINTER_NAMEmatches exactly (see above) - Ghostscript jobs may clear from the spooler before
EnumJobsever sees them — the dashboard handles this by marking such files complete immediately; check the physical printer for output to confirm
Printer spooler is unreachable
safe_get_jobs() will retry 3 times with a 5-second delay between attempts before returning an empty job list. If the printer is consistently unreachable, check that the print spooler service is running: services.msc → Print Spooler → Started.
MOVE_FILES = Falseby default. Always verify results in copy mode first.batch_printer.pyis Windows-only. It will not run on macOS or Linux.- OCR is optional across all modules. Missing
pytesseract/pdf2imageprints a warning but does not prevent the toolkit from running. - All state in
batch_printeris scoped to eachrun()call — running batch print twice in one session starts completely clean. - The destination folder in the PDF Scanner is automatically excluded from the walk even when it is inside the search root, preventing an infinite copy loop.
- Batch Print defaults to printing only page 1 of each PDF (
PRINT_FIRST_PAGE=1,PRINT_LAST_PAGE=1), intended for cover-page/letterhead printing. SetPRINT_LAST_PAGE=0to print full documents, or adjust both values to print a custom range.