Convert a scanned form PDF into a fillable AcroForm PDF.
make_fillable rasterises each page, uses OpenCV to find the places a human
would write, and injects matching interactive form fields back into the
original PDF with pikepdf. The page imagery
is left untouched — only AcroForm widgets are added — so the result looks
identical on paper but can be filled in and printed from any PDF viewer.
| Feature on the page | Field created |
|---|---|
| Bordered table grids | One text field per empty cell (header/label cells are skipped) |
| Horizontal underlines (blank write-on lines) | A single text field sitting just above the line |
Date blanks ___/___/___ |
Three separate fields (month / day / year) that don't cover the slashes |
| Small hollow squares | Checkbox fields |
It also tries hard to avoid false positives: full-width decorative rules, underlined instruction text, and a grid's own ruling lines are ignored, and fields are sized to leave room for descenders so printed text clears the line.
- Python 3.9+
- poppler-utils — the
pdftoppmcommand is used to rasterise pages and must be on yourPATH. This is a system dependency, not a pip package:- Debian/Ubuntu:
sudo apt install poppler-utils - Fedora:
sudo dnf install poppler-utils - macOS (Homebrew):
brew install poppler
- Debian/Ubuntu:
- Python dependencies (installed automatically by pip):
opencv-python-headless,numpy,pikepdf.
pipx installs the command into its own isolated
environment and puts make_fillable on your PATH — and it works on modern
Debian/Ubuntu where the system Python is "externally managed" (PEP 668) and a
plain pip install into it is refused. From the project directory:
pipx install .To reinstall / upgrade after changes:
pipx install --force .(sudo apt install pipx first if you don't have it.)
From the project directory:
pip install .Or an editable/development install:
pip install -e .Note on OpenCV and externally-managed Pythons: the package depends on
opencv-python-headless(no GUI libraries). On Debian/Ubuntu the system Python is externally managed, sopip installmust target a virtualenv — and a fresh venv will try to download the (large) OpenCV wheel. If your system already provides OpenCV/numpy/pikepdf via apt (python3-opencv,python3-pikepdf,python3-numpy), reuse them by creating the venv from the system interpreter with--system-site-packagesand installing with--no-deps(no downloads required):python3 -m venv --system-site-packages .venv .venv/bin/pip install --no-deps .The apt packages must match the interpreter the venv is built from: a venv created by a different Python (e.g. one
uvmanages) cannot reuse them and will fall back to downloading the wheels.
make_fillable input.pdf output.pdfEquivalently, as a module:
python -m make_fillable input.pdf output.pdfExample output:
Rendering pages from input.pdf …
Page 1/11 … 0 cell, 33 text, 3 date, 20 checkbox fields
...
Saved → output.pdf (844 fields total)
from pathlib import Path
from make_fillable import process
process(Path("input.pdf"), Path("output.pdf"))Individual detection helpers (detect_tables, detect_checkboxes,
classify_lines, …) are importable from make_fillable.core if you want to
build a custom pipeline.
Detection thresholds are module-level constants at the top of
src/make_fillable/core.py — e.g. underline width
ratios, checkbox size range, minimum field height. They are tuned for
300 DPI Letter-size scans of typical printed forms; adjust them if your
documents differ (denser grids, smaller checkboxes, etc.).
pdftoppmrenders every page to a 300 DPI grayscale image.- OpenCV morphology isolates horizontal/vertical rules to reconstruct table grids; connected components of the non-grid area become candidate cells.
- Remaining horizontal strokes are filtered into genuine write-on underlines,
then classified as plain text or date (
___/___/___) fields. - Hollow squares are found via contour hierarchy (an outer box with a large empty hole) and turned into checkboxes.
- Image coordinates are mapped back to PDF points and written as AcroForm widget annotations with the Print flag set, so entries appear both on screen and in print.
- Field names encode their page and a running index
(
text_1_4,date_1_9_month,cell_4_57,cb_3_45), so they are unique and stable for a given input. - Filled values render and print reliably from full PDF viewers (Okular,
Evince, Firefox, Adobe Reader). Some lightweight viewers honour
NeedAppearancesinconsistently; if entries don't print, fill and print from one of the above or flatten the form first. - Detection is heuristic. Very faint scans, skew, or unusual layouts may need threshold tuning.
The repository ships pre-commit hooks that lint and type-check on every commit:
- ruff — lints the Python code (
src/) - mypy — type-checks the Python code (
src/) - markdownlint — lints the Markdown (
README.md)
Enable them once after cloning:
pip install pre-commit # or: pipx install pre-commit
pre-commit installRun them against everything on demand:
pre-commit run --all-filesTool settings live in pyproject.toml ([tool.ruff],
[tool.mypy]) and .markdownlint.yaml; hook versions are
pinned in .pre-commit-config.yaml and can be bumped
with pre-commit autoupdate.
Ruff runs with select = ["ALL"]. Before changing how lint findings are handled,
read the linting policy in CONTRIBUTING.md: fix what is
fixable, and otherwise suppress per-line with an explanatory # noqa /
# type: ignore — never via global or per-file ignores.
MIT — see LICENSE.