Skip to main content

typst-fillable

CI codecov PyPI version Python 3.10+ License: MIT

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

  1. 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.

  2. Metadata Extraction: When generating a PDF, typst-fillable queries the template using typst.query() to extract all field metadata.

  3. Overlay Creation: ReportLab creates a transparent PDF overlay with interactive AcroForm fields at the exact positions specified in the metadata.

  4. 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 file
  • context: Optional dict to pass as context.json to the template
  • root: Root directory for Typst compilation
  • pdf_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 checkboxes
  • survey/ - Customer satisfaction survey with rating scales (1-5) and multiple choice
  • contract/ - Service agreement with signature boxes and legal checkboxes
  • invoice/ - 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

  1. Fork the repository
  2. Create a new branch (git checkout -b feature/amazing-feature)
  3. Set up your development environment (see Development section above)
  4. Make your changes
  5. Run tests and checks to ensure everything passes:
    pytest
    ruff check .
    mypy src/
    
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to your branch (git push origin feature/amazing-feature)
  8. 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!

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typst_fillable-0.0.1.tar.gz (94.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typst_fillable-0.0.1-py3-none-any.whl (14.7 kB view details)

Uploaded Python 3

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

Hashes for typst_fillable-0.0.1.tar.gz
Algorithm Hash digest
SHA256 7ffafa8f138e7ee41b2e2cae6af57d7b4e40ea013346c22adff01a6b43787b2c
MD5 3b97188a24d1a4cdb2547b81f28387a1
BLAKE2b-256 43ed3c82a95d8aaee4f3a56791da1428790f07f52e3564bbbf83cdb53373cb63

See more details on using hashes here.

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

Hashes for typst_fillable-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 81623c92860319ef0c9e54b5b2bdda0d08f4f834d7dc0433d12c2330075b46a4
MD5 dea0a5363a41295955a5e66d18a38fd3
BLAKE2b-256 327c94c6d6ee8885517889365fca15e3fa9929505413bd043ad8defcfdbb20ef

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page