Read and write PDFs with Python, powered by qpdf
—
Interactive PDF elements including form fields, annotations, and user input handling with comprehensive field type support. These capabilities enable creation and manipulation of interactive PDF documents.
The AcroForm class provides comprehensive PDF form management including field creation, modification, and appearance generation.
class AcroForm:
"""
PDF form (AcroForm) manager for interactive form handling.
Provides access to form fields, appearance generation, and
form-level operations for PDF documents.
"""
@property
def exists(self) -> bool:
"""
Whether the PDF contains an interactive form.
Returns:
bool: True if the PDF has an AcroForm dictionary
"""
@property
def fields(self) -> list[AcroFormField]:
"""
List of all form fields in the PDF.
Returns:
list[AcroFormField]: All form fields including nested fields
"""
@property
def needs_appearances(self) -> bool:
"""
Whether form field appearances need to be generated.
When True, PDF viewers should generate field appearances.
When False, appearances are already present in the PDF.
Returns:
bool: Current NeedAppearances flag state
"""
@needs_appearances.setter
def needs_appearances(self, value: bool) -> None:
"""
Set whether form field appearances need to be generated.
Parameters:
- value (bool): New NeedAppearances flag value
"""
def add_field(self, field: AcroFormField) -> None:
"""
Add a form field to the PDF.
Parameters:
- field (AcroFormField): Field to add to the form
Raises:
ValueError: If field is invalid or conflicts with existing fields
"""
def remove_fields(self, names: list[str]) -> None:
"""
Remove form fields by their fully qualified names.
Parameters:
- names (list[str]): List of field names to remove
Raises:
KeyError: If any specified field name is not found
"""
def generate_appearances_if_needed(self) -> None:
"""
Generate appearances for fields that need them.
This method should be called before saving if any field
values have been modified programmatically.
"""Individual form field objects with type-specific operations and value management.
class AcroFormField:
"""
Individual PDF form field with type-specific operations.
Represents a single form field such as text field, checkbox,
radio button, choice field, or signature field.
"""
@property
def field_type(self) -> str:
"""
The form field type.
Returns:
str: Field type ('Tx' for text, 'Btn' for button, 'Ch' for choice, 'Sig' for signature)
"""
@property
def fully_qualified_name(self) -> str:
"""
The field's fully qualified name including parent hierarchy.
Returns:
str: Complete field name path (e.g., 'form.section.fieldname')
"""
@property
def partial_name(self) -> str:
"""
The field's partial name (not including parent hierarchy).
Returns:
str: Field's own name without parent path
"""
@property
def value(self) -> Object:
"""
The field's current value.
Returns:
Object: Field value (type depends on field type)
"""
@value.setter
def value(self, new_value: Object) -> None:
"""
Set the field's value.
Parameters:
- new_value (Object): New value for the field
"""
@property
def default_value(self) -> Object:
"""
The field's default value.
Returns:
Object: Default value for reset operations
"""
@property
def is_text(self) -> bool:
"""
Whether this is a text field.
Returns:
bool: True if field type is 'Tx' (text field)
"""
@property
def is_checkbox(self) -> bool:
"""
Whether this is a checkbox field.
Returns:
bool: True if this is a checkbox button field
"""
@property
def is_radiobutton(self) -> bool:
"""
Whether this is a radio button field.
Returns:
bool: True if this is a radio button field
"""
@property
def is_choice(self) -> bool:
"""
Whether this is a choice field (list box or combo box).
Returns:
bool: True if field type is 'Ch' (choice field)
"""
@property
def is_signature(self) -> bool:
"""
Whether this is a signature field.
Returns:
bool: True if field type is 'Sig' (signature field)
"""
@property
def flags(self) -> int:
"""
Form field flags as a bitmask.
Returns:
int: Field flags (readonly, required, etc.)
"""
@property
def rect(self) -> Rectangle:
"""
Field's bounding rectangle on the page.
Returns:
Rectangle: Field's position and size
"""
def set_value(self, value) -> None:
"""
Set the field's value with automatic type conversion.
Parameters:
- value: New value (automatically converted to appropriate PDF object type)
"""
def generate_appearance(self) -> None:
"""
Generate the field's appearance stream.
Creates the visual representation of the field based on
its current value and formatting properties.
"""PDF annotation objects for interactive elements and markup.
class Annotation(Object):
"""
PDF annotation object for interactive elements and markup.
Annotations include form fields, comments, highlights, links,
and other interactive or markup elements.
"""
@property
def subtype(self) -> Name:
"""
The annotation's subtype.
Returns:
Name: Annotation subtype (e.g., Name.Widget, Name.Link, Name.Text)
"""
@property
def rect(self) -> Rectangle:
"""
The annotation's bounding rectangle.
Returns:
Rectangle: Position and size of the annotation
"""
@property
def flags(self) -> int:
"""
Annotation flags as a bitmask.
Returns:
int: Flags controlling annotation behavior and appearance
"""
@property
def appearance_dict(self) -> Dictionary:
"""
The annotation's appearance dictionary.
Contains appearance streams for different annotation states.
Returns:
Dictionary: Appearance dictionary with normal, rollover, and down states
"""
@property
def contents(self) -> String:
"""
The annotation's text content or description.
Returns:
String: Textual content associated with the annotation
"""
@property
def page(self) -> Page:
"""
The page containing this annotation.
Returns:
Page: Page object where this annotation is placed
"""
def get_appearance_stream(self, which: str = 'N') -> Stream:
"""
Get an appearance stream for the annotation.
Parameters:
- which (str): Appearance state ('N' for normal, 'R' for rollover, 'D' for down)
Returns:
Stream: Appearance stream for the specified state
Raises:
KeyError: If the requested appearance state doesn't exist
"""
def get_page_content_for_appearance(self) -> bytes:
"""
Get page content suitable for use in appearance generation.
Returns:
bytes: Content stream data for appearance generation
"""Enumeration of form field flags for controlling field behavior.
from enum import IntFlag
class FormFieldFlag(IntFlag):
"""Form field flags controlling field behavior and appearance."""
readonly = ... # Field is read-only
required = ... # Field is required
noexport = ... # Field value is not exported
multiline = ... # Text field allows multiple lines
password = ... # Text field is a password field
notoggletooff = ... # Radio button cannot be turned off
radio = ... # Button field is a radio button
pushbutton = ... # Button field is a push button
combo = ... # Choice field is a combo box
edit = ... # Choice field allows text editing
sort = ... # Choice field options should be sorted
fileselect = ... # Text field is for file selection
multiselect = ... # Choice field allows multiple selections
donotspellcheck = ... # Field content should not be spell-checked
donotscroll = ... # Text field should not scroll
comb = ... # Text field is a comb field
richtext = ... # Text field supports rich text formatting
radios_in_unison = ... # Radio buttons act in unison
commit_on_sel_change = ... # Commit value on selection changeEnumeration of annotation flags for controlling annotation behavior.
class AnnotationFlag(IntFlag):
"""Annotation flags controlling annotation behavior and appearance."""
invisible = ... # Annotation is invisible
hidden = ... # Annotation is hidden
print_ = ... # Annotation should be printed
nozoom = ... # Annotation should not scale with zoom
norotate = ... # Annotation should not rotate with page
noview = ... # Annotation should not be displayed
readonly = ... # Annotation is read-only
locked = ... # Annotation is locked
togglenoview = ... # Toggle view state on mouse click
lockedcontents = ... # Annotation contents are lockedimport pikepdf
# Open a PDF with form fields
pdf = pikepdf.open('form_document.pdf')
# Access the form
form = pdf.acroform
if form.exists:
print(f"Form has {len(form.fields)} fields")
# Iterate through all fields
for field in form.fields:
name = field.fully_qualified_name
field_type = field.field_type
value = field.value
print(f"Field '{name}' (type: {field_type}): {value}")
# Check field properties
if field.is_text:
print(f" Text field with value: {value}")
elif field.is_checkbox:
print(f" Checkbox is {'checked' if value else 'unchecked'}")
elif field.is_choice:
print(f" Choice field with selection: {value}")
pdf.close()import pikepdf
pdf = pikepdf.open('form_document.pdf')
form = pdf.acroform
if form.exists:
for field in form.fields:
name = field.fully_qualified_name
# Set text field values
if field.is_text and 'name' in name.lower():
field.set_value("John Doe")
elif field.is_text and 'email' in name.lower():
field.set_value("john.doe@example.com")
# Check/uncheck checkboxes
elif field.is_checkbox and 'agree' in name.lower():
field.set_value(True) # Check the box
# Set choice field selections
elif field.is_choice and 'country' in name.lower():
field.set_value("United States")
# Generate field appearances after modification
form.generate_appearances_if_needed()
# Flatten form (make fields non-editable)
# form.remove_fields([field.fully_qualified_name for field in form.fields])
pdf.save('filled_form.pdf')
pdf.close()import pikepdf
pdf = pikepdf.open('document.pdf')
page = pdf.pages[0]
# Ensure the PDF has a form
if not pdf.acroform.exists:
# Create form structure
pdf.Root['/AcroForm'] = pikepdf.Dictionary({
'/Fields': pikepdf.Array(),
'/NeedAppearances': True
})
# Create a text field
text_field = pikepdf.Dictionary({
'/Type': pikepdf.Name.Annot,
'/Subtype': pikepdf.Name.Widget,
'/FT': pikepdf.Name.Tx, # Text field
'/T': pikepdf.String('username'), # Field name
'/V': pikepdf.String(''), # Default value
'/Rect': pikepdf.Array([100, 700, 300, 720]), # Position and size
'/P': page # Parent page
})
# Create a checkbox field
checkbox_field = pikepdf.Dictionary({
'/Type': pikepdf.Name.Annot,
'/Subtype': pikepdf.Name.Widget,
'/FT': pikepdf.Name.Btn, # Button field
'/Ff': 0, # Not a radio button or push button (checkbox)
'/T': pikepdf.String('subscribe'),
'/V': pikepdf.Name.Off, # Unchecked
'/Rect': pikepdf.Array([100, 650, 120, 670]),
'/P': page
})
# Add fields to form and page
pdf.Root['/AcroForm']['/Fields'].extend([text_field, checkbox_field])
# Add annotations to page
if '/Annots' not in page:
page['/Annots'] = pikepdf.Array()
page['/Annots'].extend([text_field, checkbox_field])
pdf.save('document_with_form.pdf')
pdf.close()import pikepdf
pdf = pikepdf.open('document.pdf')
page = pdf.pages[0]
# Check if page has annotations
if '/Annots' in page:
annotations = page['/Annots']
for annot_ref in annotations:
annot = annot_ref # Resolve if indirect
# Check annotation type
subtype = annot.get('/Subtype')
if subtype == pikepdf.Name.Link:
# Handle link annotation
rect = annot['/Rect']
action = annot.get('/A')
print(f"Link at {rect}: {action}")
elif subtype == pikepdf.Name.Text:
# Handle text annotation (note/comment)
contents = annot.get('/Contents', '')
print(f"Note: {contents}")
elif subtype == pikepdf.Name.Widget:
# Handle form field widget
field_name = annot.get('/T', 'unnamed')
field_type = annot.get('/FT')
print(f"Form field '{field_name}' of type {field_type}")
pdf.close()import pikepdf
import re
pdf = pikepdf.open('form_document.pdf')
form = pdf.acroform
def validate_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
if form.exists:
errors = []
for field in form.fields:
name = field.fully_qualified_name
value = str(field.value) if field.value else ""
# Check required fields
if field.flags & pikepdf.FormFieldFlag.required:
if not value.strip():
errors.append(f"Required field '{name}' is empty")
# Validate email fields
if 'email' in name.lower() and value:
if not validate_email(value):
errors.append(f"Invalid email in field '{name}': {value}")
# Check text field length limits
if field.is_text and hasattr(field, 'max_length'):
if len(value) > field.max_length:
errors.append(f"Field '{name}' exceeds maximum length")
if errors:
print("Form validation errors:")
for error in errors:
print(f" - {error}")
else:
print("Form validation passed")
pdf.close()import pikepdf
pdf = pikepdf.open('complex_form.pdf')
form = pdf.acroform
# Group fields by type
field_groups = {
'text': [],
'checkbox': [],
'radio': [],
'choice': [],
'signature': []
}
if form.exists:
for field in form.fields:
if field.is_text:
field_groups['text'].append(field)
elif field.is_checkbox:
field_groups['checkbox'].append(field)
elif field.is_radiobutton:
field_groups['radio'].append(field)
elif field.is_choice:
field_groups['choice'].append(field)
elif field.is_signature:
field_groups['signature'].append(field)
# Report field statistics
for field_type, fields in field_groups.items():
print(f"{field_type.capitalize()} fields: {len(fields)}")
for field in fields:
print(f" - {field.fully_qualified_name}")
# Batch operations
# Make all text fields read-only
for field in field_groups['text']:
field.flags |= pikepdf.FormFieldFlag.readonly
# Clear all checkbox values
for field in field_groups['checkbox']:
field.set_value(False)
# Set default selections for choice fields
for field in field_groups['choice']:
if hasattr(field, 'options') and field.options:
field.set_value(field.options[0]) # Select first option
# Generate appearances after modifications
form.generate_appearances_if_needed()
pdf.save('processed_form.pdf')
pdf.close()Install with Tessl CLI
npx tessl i tessl/pypi-pikepdf