privacyforms-pdf
Python library for parsing and filling PDF forms using pypdf.
Features
- Parse fillable PDFs into a canonical
PDFRepresentationschema - Fill PDF forms from simple JSON key/value data
- Extract layout hints and visual row groupings
- Validate representation JSON against the schema
- Verify sample data keys against parsed field IDs
- Extend the CLI through
pluggycommand entry points
Requirements
- Python
3.12+ pypdf >= 5
Installation
git clone <repo-url>
cd privacyforms.pdf
uv sync
CLI Quick Start
Parse A PDF
pdf-forms parse form.pdf -o representation.json
This writes a compact PDFRepresentation JSON document.
Verify A Representation JSON File
pdf-forms verify-json representation.json
Verify Sample Data Keys Against Parsed Field IDs
pdf-forms verify-data --form-json representation.json --data-json sample-data.json
Preferred format:
- use field IDs such as
f-0for canonical machine-facing data - field names such as
Candidate Nameremain supported for convenience
Compatibility modes:
fill-formaccepts--field-keys name|id|autoverify-dataaccepts--key-mode name|id|autoautoaccepts a mixture of field IDs and field names
Fill A PDF Form
pdf-forms fill-form form.pdf data.json -o filled.pdf
pdf-forms fill-form form.pdf data.json -o filled.pdf --no-validate
pdf-forms fill-form form.pdf data.json -o filled.pdf --strict
pdf-forms fill-form form.pdf data.json -o filled.pdf --field-keys id
Recommended fill-form payloads are keyed by field IDs:
{
"f-0": "John Smith",
"f-1": "Software Engineer",
"f-2": "2025-06-01",
"f-3": true
}
Field names and mixed key styles are still supported through --field-keys name and --field-keys auto.
Check Whether A PDF Contains A Form
pdf-forms info form.pdf
Python API
The package currently exposes two main API layers:
- read/parse APIs via
parse_pdf()andextract_pdf_form() - higher-level read/fill/validate APIs via
PDFFormService
Parse A PDF Into PDFRepresentation
from privacyforms_pdf import extract_pdf_form
representation = extract_pdf_form("form.pdf")
print(representation.spec_version)
print(representation.source)
print(len(representation.fields))
print(len(representation.rows))
for field in representation.fields:
print(field.id, field.name, field.type, field.value)
Extract Labels And Nearby Text
Install the optional labels dependency (requires PyMuPDF):
pip install privacyforms.pdf[labels]
CLI:
pdf-forms parse form.pdf --labels -o representation.json
Python API:
from privacyforms_pdf import PDFFormService
service = PDFFormService()
representation = service.extract("form.pdf", extract_labels=True)
for field in representation.fields:
print(field.title) # best inferred label
for block in field.text_blocks:
print(block.role, block.direction, block.text)
You can also call parse_pdf() directly:
from privacyforms_pdf import parse_pdf
representation = parse_pdf("form.pdf")
json_text = representation.to_compact_json()
Fill And Validate Forms
from privacyforms_pdf import PDFFormService
service = PDFFormService()
has_form = service.has_form("form.pdf")
form_data = {
"f-0": "John Smith",
"f-3": True,
}
errors = service.validate_form_data("form.pdf", form_data, key_mode="id")
if errors:
print(errors)
else:
service.fill_form("form.pdf", form_data, "filled.pdf", key_mode="id")
You can also fill from a JSON file:
from privacyforms_pdf import PDFFormService
service = PDFFormService()
service.fill_form_from_json("form.pdf", "data.json", "filled.pdf", key_mode="id")
The class also exposes read helpers:
from privacyforms_pdf import PDFFormService
service = PDFFormService()
representation = service.extract("form.pdf")
fields = service.list_fields("form.pdf")
field = service.get_field_by_id("form.pdf", "f-0")
value = service.get_field_value("form.pdf", "Candidate Name")
service.extract_to_json("form.pdf", "representation.json")
Public Objects
Primary exports from privacyforms_pdf:
PDFFormServiceFormFillerparse_pdfextract_pdf_formPDFRepresentationPDFFieldFieldFlagsFieldLayoutFieldTextBlockFieldTextRoleFieldTextDirectionChoiceOptionRowGroupPDFFormErrorPDFFormNotFoundErrorFormValidationErrorFieldNotFoundError
PDFRepresentation Schema
Top-level fields:
spec_version: strsource: str | Nonefields: list[PDFField]rows: list[RowGroup]
PDFField
Main fields:
name: strtitle: str | Noneid: strtype: PDFFieldTypefield_flags: FieldFlags | Nonelayout: FieldLayout | Nonedefault_value: str | bool | list[str] | Nonevalue: str | bool | list[str] | Nonechoices: list[ChoiceOption]text_blocks: list[FieldTextBlock]— nearby labels, descriptions, helpersformat: str | Nonemax_length: int | Nonetextarea_rows: int | Nonetextarea_cols: int | None
Supported field types:
textfieldtextareadatefieldcheckboxradiobuttongroupcomboboxlistboxsignature
FieldLayout
Layout hints are stored in integer PDF coordinates:
page: int | Nonex: int | Noney: int | Nonewidth: int | Noneheight: int | None
FieldTextBlock
Nearby text associated with a field (populated when extract_labels=True):
text: strrole: "label" | "description" | "helper" | "instruction" | "unknown"direction: "left" | "right" | "above" | "below" | "inside" | "unknown"layout: FieldLayout | Nonedistance: float | None
RowGroup
Visual rows derived from layout analysis:
fields: list[PDFField | str]page_index: int
When serialized, row fields are emitted as field IDs.
JSON Shape
Example parsed representation:
{
"source": "form.pdf",
"fields": [
{
"name": "Candidate Name",
"id": "f-0",
"type": "textfield",
"layout": {
"page": 1,
"x": 53,
"y": 1077,
"width": 361,
"height": 27
}
}
],
"rows": [
{
"fields": ["f-0"],
"page_index": 1
}
]
}
Notes:
- omitted fields are intentionally excluded by compact serialization
field_flagsonly serializes flags set totruerowsreference fields by ID in JSON
Exceptions
PDFFormError: base exception for form-related failuresPDFFormNotFoundError: raised when a PDF does not contain a formFormValidationError: raised when fill-time validation failsFieldNotFoundError: exported for compatibility and field lookup failures
Ratings
| Aspect | Score | Notes |
|---|---|---|
| Overall | 9/10 | Production-grade library with excellent engineering discipline |
| Security | 9/10 | Input validation, symlink rejection, size limits, Bandit clean |
| Architecture | 9/10 | Clean layers, canonical schema, pluggy extensibility |
| API Design | 8/10 | Dual function/class layers, type-safe, minor wrapper leakage |
| Functionality | 9/10 | All form types handled, cross-generator radio support, graceful fallback |
| Code Quality | 9/10 | 100% coverage, strict ruff/ty, complete type hints |
| Documentation | 8/10 | Excellent project docs; PDF internals could use more inline depth |
Security
- Symlinks are rejected for both reads and writes to prevent path-traversal issues
- PDF files are validated via magic-byte header check (
%PDF) before parsing - Input size limits guard against oversized PDFs (> 50 MB) and JSON (> 10 MB)
- JSON depth limits prevent stack exhaustion from malicious payloads
Architecture
- Clean separation of concerns:
schema→parser→filler→extractor→cli - Canonical
PDFRepresentationschema (Pydantic v2) is the single source of truth - CLI commands are loaded dynamically via
pluggyentry points — easy to extend - Low-level PDF writer (
FormFiller) is decoupled from the high-level service (PDFFormService)
API Design
- Two complementary layers: function-based (
parse_pdf,extract_pdf_form) and class-based (PDFFormService) - Field IDs are the canonical key format; field names remain supported for convenience
key_mode="auto"accepts mixed payloads of IDs and names- All public methods have complete type hints and Google-style docstrings
Functionality
- Handles all common PDF form types: text, textarea, date, checkbox, radio, combo, listbox, signature
- Radio button state resolution works across different PDF generators
- Listbox filling includes custom appearance streams so selections are visible in viewers
- Graceful fallback when pypdf's appearance-stream generation hits edge cases
Code Quality
- 100% test coverage (426 tests) with pytest and
pytest-cov - Ruff enforces strict linting (E, W, F, I, N, D, UP, B, C4, SIM, TCH)
- ty type checker runs in strict mode — complete type hints throughout
- Bandit security scanner integrated; no high or medium severity issues
Development
Quality Checks
make check
make test
make test-cov
Project Structure
privacyforms.pdf/
├── privacyforms_pdf/
│ ├── __init__.py
│ ├── schema.py
│ ├── schema_layout.py
│ ├── parser.py
│ ├── extractor.py
│ ├── filler.py
│ ├── hooks.py
│ ├── cli.py
│ └── commands/
├── tests/
├── samples/
├── demo/
├── docs/
├── pyproject.toml
└── README.md
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file privacyforms_pdf-0.2.0.tar.gz.
File metadata
- Download URL: privacyforms_pdf-0.2.0.tar.gz
- Upload date:
- Size: 66.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ffc436db85c68e67fdd69c14abdc21f4b1318af6ed9fa77a806cc4f463be38f1
|
|
| MD5 |
263d84d613c628c27e6772486fef5a52
|
|
| BLAKE2b-256 |
6daef4f2db1f5ca6ad964a5d8dd60f957f1e212e05a9d987d0a56490e85614d3
|
Provenance
The following attestation bundles were made for privacyforms_pdf-0.2.0.tar.gz:
Publisher:
publish-pypi.yml on zopyx/privacyforms.pdf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
privacyforms_pdf-0.2.0.tar.gz -
Subject digest:
ffc436db85c68e67fdd69c14abdc21f4b1318af6ed9fa77a806cc4f463be38f1 - Sigstore transparency entry: 2347719411
- Sigstore integration time:
-
Permalink:
zopyx/privacyforms.pdf@cf6b1abe61c0a15a4dfd71a0ff5b4731ef5f2acf -
Branch / Tag:
refs/heads/master - Owner: https://github.com/zopyx
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@cf6b1abe61c0a15a4dfd71a0ff5b4731ef5f2acf -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file privacyforms_pdf-0.2.0-py3-none-any.whl.
File metadata
- Download URL: privacyforms_pdf-0.2.0-py3-none-any.whl
- Upload date:
- Size: 46.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06295939db70c8cee4826c5e625dbf442b99e50ed1c3127075fe3897229c63c8
|
|
| MD5 |
5d690f86213ebda29c1f49036b1bcea5
|
|
| BLAKE2b-256 |
55c890bf461adb3ea59d83e3c3052910abd821f061160a0bbdc2f532d0e3ca66
|
Provenance
The following attestation bundles were made for privacyforms_pdf-0.2.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on zopyx/privacyforms.pdf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
privacyforms_pdf-0.2.0-py3-none-any.whl -
Subject digest:
06295939db70c8cee4826c5e625dbf442b99e50ed1c3127075fe3897229c63c8 - Sigstore transparency entry: 2347720028
- Sigstore integration time:
-
Permalink:
zopyx/privacyforms.pdf@cf6b1abe61c0a15a4dfd71a0ff5b4731ef5f2acf -
Branch / Tag:
refs/heads/master - Owner: https://github.com/zopyx
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@cf6b1abe61c0a15a4dfd71a0ff5b4731ef5f2acf -
Trigger Event:
workflow_dispatch
-
Statement type: