Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

privacyforms-pdf

CI PyPI Code style: ruff License: MIT uv

Python library for parsing and filling PDF forms using pypdf.

Features

  • Parse fillable PDFs into a canonical PDFRepresentation schema
  • 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 pluggy command 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-0 for canonical machine-facing data
  • field names such as Candidate Name remain supported for convenience

Compatibility modes:

  • fill-form accepts --field-keys name|id|auto
  • verify-data accepts --key-mode name|id|auto
  • auto accepts 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() and extract_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:

  • PDFFormService
  • FormFiller
  • parse_pdf
  • extract_pdf_form
  • PDFRepresentation
  • PDFField
  • FieldFlags
  • FieldLayout
  • FieldTextBlock
  • FieldTextRole
  • FieldTextDirection
  • ChoiceOption
  • RowGroup
  • PDFFormError
  • PDFFormNotFoundError
  • FormValidationError
  • FieldNotFoundError

PDFRepresentation Schema

Top-level fields:

  • spec_version: str
  • source: str | None
  • fields: list[PDFField]
  • rows: list[RowGroup]

PDFField

Main fields:

  • name: str
  • title: str | None
  • id: str
  • type: PDFFieldType
  • field_flags: FieldFlags | None
  • layout: FieldLayout | None
  • default_value: str | bool | list[str] | None
  • value: str | bool | list[str] | None
  • choices: list[ChoiceOption]
  • text_blocks: list[FieldTextBlock] — nearby labels, descriptions, helpers
  • format: str | None
  • max_length: int | None
  • textarea_rows: int | None
  • textarea_cols: int | None

Supported field types:

  • textfield
  • textarea
  • datefield
  • checkbox
  • radiobuttongroup
  • combobox
  • listbox
  • signature

FieldLayout

Layout hints are stored in integer PDF coordinates:

  • page: int | None
  • x: int | None
  • y: int | None
  • width: int | None
  • height: int | None

FieldTextBlock

Nearby text associated with a field (populated when extract_labels=True):

  • text: str
  • role: "label" | "description" | "helper" | "instruction" | "unknown"
  • direction: "left" | "right" | "above" | "below" | "inside" | "unknown"
  • layout: FieldLayout | None
  • distance: 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_flags only serializes flags set to true
  • rows reference fields by ID in JSON

Exceptions

  • PDFFormError: base exception for form-related failures
  • PDFFormNotFoundError: raised when a PDF does not contain a form
  • FormValidationError: raised when fill-time validation fails
  • FieldNotFoundError: 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 PDFRepresentation schema (Pydantic v2) is the single source of truth
  • CLI commands are loaded dynamically via pluggy entry 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

Release files for privacyforms.pdf 1.0.0a2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for privacyforms.pdf 1.0.0a2
File Size Uploaded
privacyforms_pdf-1.0.0a2.tar.gz 66.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for privacyforms.pdf 1.0.0a2
File Interpreter ABI Platform
privacyforms_pdf-1.0.0a2-py3-none-any.whl Python 3 none any Details

Total release size: 113.1 kB

Release files / privacyforms_pdf-1.0.0a2.tar.gz

Download URL privacyforms_pdf-1.0.0a2.tar.gz
Size 66.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1df9d4bd6827e05e7f689b5a4ea15a9254c7a74a78b8e9e0a474ffb3bb125c61
BLAKE2b-256 checksum
How to use checksums
0a53d15a1e04b368340fe63b4c35b7475bcbf0cfca86162b3c4ab25ab6947af4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release files / privacyforms_pdf-1.0.0a2-py3-none-any.whl

Download URL privacyforms_pdf-1.0.0a2-py3-none-any.whl
Size 46.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8ffe78d861bf7b8b219c553e24924f844f89c4ec667f845c53fa9c6d6247d968
BLAKE2b-256 checksum
How to use checksums
23c2ef71fe904ca94f024b9b9dafd350e01d64db54cdf95954e070c885d60553
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0a2 This release

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page