Add interactive form fields to Typst-generated PDFs
Project description
typst-fillable
Add interactive form fields to Typst-generated PDFs.
Overview
typst-fillable is a Python library that transforms static Typst PDFs into interactive fillable forms. It extracts field position metadata embedded in Typst templates and overlays interactive AcroForm fields using ReportLab.
Key features:
- Create fillable PDFs from Typst templates
- Support for text fields, textareas, checkboxes, and radio buttons
- Customizable field styling
- Works with multi-page documents
- Pre-fill forms with data or generate blank forms
Installation
pip install typst-fillable
Requirements:
- Python 3.10+
- Typst CLI installed and available in PATH
Quick Start
1. Create a Typst template with form fields
// form.typ
#import "capture_field.typ": capture_field
#let ctx = json("context.json")
Name: #capture_field(field_name: "name", field_type: "text")[
#box(width: 200pt, height: 14pt, stroke: 0.5pt, fill: rgb("#f7f9fb"))
]
Email: #capture_field(field_name: "email", field_type: "text")[
#box(width: 200pt, height: 14pt, stroke: 0.5pt, fill: rgb("#f7f9fb"))
]
2. Generate a fillable PDF
from typst_fillable import make_fillable
# Generate blank fillable form
pdf = make_fillable(
template="form.typ",
context={},
root="./templates"
)
with open("fillable_form.pdf", "wb") as f:
f.write(pdf)
How It Works
-
Template Design: Use
capture_field()in your Typst template to mark where interactive fields should appear. The function emits metadata about field position and properties. -
Metadata Extraction: When generating a PDF,
typst-fillablequeries the template usingtypst.query()to extract all field metadata. -
Overlay Creation: ReportLab creates a transparent PDF overlay with interactive AcroForm fields at the exact positions specified in the metadata.
-
Merge: The base Typst PDF and the form overlay are merged using PyPDF to create the final fillable document.
API Reference
make_fillable()
The main entry point for generating fillable PDFs.
def make_fillable(
template: str | Path,
context: dict | None = None,
root: str | Path | None = None,
pdf_bytes: bytes | None = None,
style: FieldStyle | None = None,
) -> bytes:
Parameters:
template: Path to the Typst template filecontext: Optional dict to pass ascontext.jsonto the templateroot: Root directory for Typst compilationpdf_bytes: Pre-compiled PDF bytes (skips compilation if provided)style: Custom styling for form fields
Returns: Fillable PDF as bytes
extract_field_metadata()
Extract field positions from a Typst template.
def extract_field_metadata(
template_path: str | Path,
root: str | Path | None = None,
) -> list[FieldMetadata]:
create_form_overlay()
Create a PDF overlay with interactive form fields.
def create_form_overlay(
fields: list[FieldMetadata],
page_count: int,
page_size: tuple[float, float] = (612.0, 792.0),
style: FieldStyle | None = None,
) -> BytesIO:
merge_with_overlay()
Merge a base PDF with a form field overlay.
def merge_with_overlay(
base_pdf: bytes,
form_overlay: BytesIO,
) -> bytes:
FieldStyle
Customize form field appearance.
from typst_fillable import FieldStyle
style = FieldStyle(
fill_color="#ffffff", # Field background color
text_color="#000000", # Text color
font_size=8, # Font size in points
border_width=0, # Border width (0 for none)
)
Typst Template Guide
The capture_field() Function
#let capture_field(
field_name: "", // Unique field identifier (required)
field_type: "text", // "text", "textarea", "checkbox", or "radio"
dimensions: (:), // Custom dimensions (optional)
group_name: none, // Radio button group name
fill_cell: false, // Expand to fill table cell
position_offset: (x: 0, y: 0), // Fine-tune position
min_width: none, // Minimum width
min_height: none, // Minimum height
prefix: "", // Text before field (e.g., "$")
suffix: "", // Text after field (e.g., "%")
content // Visual content to display
) = { ... }
Field Types
Text Field
#capture_field(field_name: "company", field_type: "text")[
#box(width: 200pt, height: 14pt, stroke: 0.5pt + gray, fill: rgb("#f7f9fb"))
]
Textarea (Multiline)
#capture_field(
field_name: "comments",
field_type: "textarea",
fill_cell: true,
min_height: 50pt,
)[
#box(width: 100%, height: 50pt, stroke: 0.5pt + gray, fill: rgb("#f7f9fb"))
]
Checkbox
#capture_field(field_name: "agree", field_type: "checkbox")[
#box(width: 12pt, height: 12pt, stroke: 0.5pt + gray, fill: rgb("#f7f9fb"))
]
Radio Buttons
// Same group_name links radio buttons together
#capture_field(field_name: "yes", field_type: "radio", group_name: "answer")[
#box(width: 10pt, height: 10pt, stroke: 0.5pt + gray, radius: 50%)
] Yes
#capture_field(field_name: "no", field_type: "radio", group_name: "answer")[
#box(width: 10pt, height: 10pt, stroke: 0.5pt + gray, radius: 50%)
] No
Fields with Prefix/Suffix
#capture_field(
field_name: "price",
field_type: "text",
prefix: "$",
suffix: ".00",
)[
#box(width: 80pt, height: 14pt, stroke: 0.5pt + gray, fill: rgb("#f7f9fb"))
]
Table Cell Fields
For fields inside table cells that should expand to fill the cell:
#table(
columns: (1fr, 1fr),
[Label],
capture_field(
field_name: "value",
field_type: "text",
fill_cell: true,
position_offset: (x: -5, y: 5),
)[
#text[#ctx.at("value", default: "")]
],
)
Examples
See the examples/ directory for complete working examples:
contact_form/- Professional contact form with sections, radio buttons, and checkboxessurvey/- Customer satisfaction survey with rating scales (1-5) and multiple choicecontract/- Service agreement with signature boxes and legal checkboxesinvoice/- Invoice with line items table, currency fields, and totals
Each example can be run with:
cd examples/<name>
python generate.py
Development
Setting up the development environment
This project uses uv for fast and reliable Python package management. If you don't have uv installed yet:
# Install uv (macOS/Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or with pip
pip install uv
Clone and setup
# Clone the repository
git clone https://github.com/carpe-diem/typst-fillable.git
cd typst-fillable
# Create a virtual environment and install dependencies with uv
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install the package in editable mode with dev dependencies
uv pip install -e ".[dev]"
Alternative: Using pip
If you prefer to use pip:
# Create virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install development dependencies
pip install -e ".[dev]"
Running tests and checks
# Run tests
pytest
# Run tests with coverage report
pytest --cov=src/typst_fillable --cov-report=term-missing
# Run linter
ruff check .
# Auto-fix linting issues
ruff check . --fix
# Format code
ruff format .
# Type check
mypy src/
Project structure
typst-fillable/
├── src/typst_fillable/ # Main package source code
├── tests/ # Test suite
├── examples/ # Example forms and usage
├── pyproject.toml # Project configuration
└── README.md # This file
Contributing
Contributions are welcome! Here's how you can help:
Reporting bugs
If you find a bug, please open an issue with:
- A clear description of the problem
- Steps to reproduce the issue
- Expected vs actual behavior
- Your Python and Typst versions
Suggesting features
Feature requests are welcome! Please open an issue describing:
- The use case for the feature
- How it would work
- Any alternatives you've considered
Submitting pull requests
- Fork the repository
- Create a new branch (
git checkout -b feature/amazing-feature) - Set up your development environment (see Development section above)
- Make your changes
- Run tests and checks to ensure everything passes:
pytest ruff check . mypy src/
- Commit your changes (
git commit -m 'Add amazing feature') - Push to your branch (
git push origin feature/amazing-feature) - Open a Pull Request
Code style
- We use Ruff for linting and formatting
- We use mypy for type checking
- Follow PEP 8 guidelines
- Add type hints to all functions
- Write docstrings for public APIs
- Keep line length to 100 characters
Testing
- Write tests for new features and bug fixes
- Ensure test coverage remains high
- Use descriptive test names
- Add integration tests for end-to-end scenarios
License
MIT License - see LICENSE for details.
Contributors
Author
Alberto Paparelli (@carpe-diem)
If you find this project useful, please consider giving it a star!
Project details
Release history Release notifications | RSS feed
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 typst_fillable-0.0.1.tar.gz.
File metadata
- Download URL: typst_fillable-0.0.1.tar.gz
- Upload date:
- Size: 94.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7ffafa8f138e7ee41b2e2cae6af57d7b4e40ea013346c22adff01a6b43787b2c
|
|
| MD5 |
3b97188a24d1a4cdb2547b81f28387a1
|
|
| BLAKE2b-256 |
43ed3c82a95d8aaee4f3a56791da1428790f07f52e3564bbbf83cdb53373cb63
|
File details
Details for the file typst_fillable-0.0.1-py3-none-any.whl.
File metadata
- Download URL: typst_fillable-0.0.1-py3-none-any.whl
- Upload date:
- Size: 14.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81623c92860319ef0c9e54b5b2bdda0d08f4f834d7dc0433d12c2330075b46a4
|
|
| MD5 |
dea0a5363a41295955a5e66d18a38fd3
|
|
| BLAKE2b-256 |
327c94c6d6ee8885517889365fca15e3fa9929505413bd043ad8defcfdbb20ef
|