Skip to main content

formfyxer.pdf_wrangling

annotations

io

math

re

statistics

Enum

tempfile

copy

Any

Callable

Dict

Iterable

Optional

List

Union

Tuple

BinaryIO

Mapping

TypedDict

cast

Sequence

Path

random

cv2

config

get_checkboxes

np

convert_from_path

pikepdf

Pdf

canvas

magenta

pink

blue

PDFLayoutAnalyzer

TextConverter

LAParams

LTPage

LTTextBoxHorizontal

LTChar

LTContainer

LTAnno

LTText

LTTextBox

LTTextBoxVertical

LTTextGroup

LTTextLine

LTImage

LTItem

PDFUnicodeNotDefined

PDFDocument

PDFResourceManager

PDFPageInterpreter

PDFPage

PDFParser

PDFDevice

resolve1

PSLiteral

PSKeyword

decode_text

translate_matrix

mult_matrix

MATRIX_IDENTITY

DEBUG

FieldType Objects

class FieldType(Enum)

TEXT

Text input Field

AREA

Text input Field, but an area

SIGNATURE

CHECK_BOX

LIST_BOX

allows multiple selection

CHOICE

allows only one selection

RADIO

__str__

def __str__() -> str

PikeField Objects

class PikeField(TypedDict)

type: str

var_name: str

all: pikepdf.objects.Object

BoundingBox

BoundingBoxF

XYPair

_to_int_bbox

def _to_int_bbox(bbox: BoundingBoxF) -> BoundingBox

FormField Objects

class FormField()

A data holding class, used to easily specify how a PDF form field should be created.

__init__

def __init__(field_name: str,
type_name: Union[FieldType, str],
x: int,
y: int,
font_size: Optional[int] = None,
tooltip: str = "",
configs: Optional[Dict[str, Any]] = None)

Constructor

Arguments

  • x - the x position of the lower left corner of the field. Should be in X,Y coordinates, where (0, 0) is the lower left of the page, x goes to the right, and units are in points (1/72th of an inch)
  • y - the y position of the lower left corner of the field. Should be in X,Y coordinates, where (0, 0) is the lower left of the page, y goes up, and units are in points (1/72th of an inch)
  • config - a dictionary containing any keyword argument to the reportlab field functions, which will vary depending on what type of field this is. See section 4.7 of the reportlab User Guide
  • field_name - the name of the field, exposed to via most APIs. Not the tooltip, but users1_name__0

make_textbox

@classmethod
def make_textbox(cls, label: str, field_bbox: BoundingBox,
font_size: int) -> "FormField"

make_textarea

@classmethod
def make_textarea(cls, label: str, field_bbox: BoundingBox,
font_size: int) -> "FormField"

make_checkbox

@classmethod
def make_checkbox(cls, label: str, bbox: BoundingBox) -> "FormField"

from_pikefield

@classmethod
def from_pikefield(cls, pike_field: PikeField) -> "FormField"

get_bbox

def get_bbox() -> BoundingBoxF

__str__

def __str__()

__repr__

def __repr__()

_create_only_fields

def _create_only_fields(io_obj,
fields_per_page: Iterable[Iterable[FormField]],
font_name: str = "Courier",
font_size: int = 20)

Creates a PDF that contains only AcroForm fields. This PDF is then merged into an existing PDF to add fields to it. We're adding fields to a PDF this way because reportlab isn't able to read PDFs, but is the best feature library for writing them.

_normalize_signature_fields

def _normalize_signature_fields(pdf: Pdf,
signature_field_names: Iterable[str]) -> None

Convert ReportLab text fields into PDF signature fields.

ReportLab does not expose an AcroForm signature widget API, so _create_only_fields creates signature placeholders as text fields. This function changes those generated widgets to /FT /Sig before they are copied into the destination PDF.

set_fields

def set_fields(in_file: Union[str, Path, BinaryIO],
out_file: Union[str, Path, BinaryIO],
fields_per_page: Iterable[Iterable[FormField]],
*,
overwrite=False)

Adds fields per page to the in_file PDF, writing the new PDF to a new file.

Example usage:

set_fields('no_fields.pdf', 'four_fields_on_second_page.pdf',
[
[], # nothing on the first page
[ # Second page
FormField('new_field', 'text', 110, 105, configs={'width': 200, 'height': 30}),
# Choice needs value to be one of the possible options, and options to be a list of strings or tuples
FormField('new_choices', 'choice', 110, 400, configs={'value': 'Option 1', 'options': ['Option 1', 'Option 2']}),
# Radios need to have the same name, with different values
FormField('new_radio1', 'radio', 110, 600, configs={'value': 'option a'}),
FormField('new_radio1', 'radio', 110, 500, configs={'value': 'option b'})
]
]
)

Arguments

  • in_file - the input file name or path of a PDF that we're adding the fields to
  • out_file - the output file name or path where the new version of in_file will be written. Doesn't need to exist.
  • fields_per_page - for each page, a series of fields that should be added to that page.
  • owerwrite - if the input file already some fields (AcroForm fields specifically) and this value is true, it will erase those existing fields and just add fields_per_page. If not true and the input file has fields, this won't generate a PDF, since there isn't currently a way to merge AcroForm fields from different PDFs.

Returns

Nothing.

rename_pdf_fields

def rename_pdf_fields(in_file: Union[str, Path, BinaryIO],
out_file: Union[str, Path, BinaryIO],
mapping: Mapping[str, str]) -> None

Given a dictionary that maps old to new field names, rename the AcroForm field with a matching key to the specified value.

Example

rename_pdf_fields('current.pdf', 'new_field_names.pdf',
\{'abc123': 'user1_name', 'abc124', 'user1_address_city'\})

Args:
in_file: the filename of an input file
out_file: the filename of the output file. Doesn't need to exist,
will be overwritten if it does exist.
mapping: the python dict that maps from a current field name to the desired name

Returns:
Nothing

<a id="formfyxer.pdf_wrangling._get_named_parent"></a>

### \_get\_named\_parent

```python
def _get_named_parent(field) -> Optional[Any]

unlock_pdf_in_place

def unlock_pdf_in_place(in_file: Union[str, Path, BinaryIO]) -> None

Try using pikePDF to unlock the PDF it it is locked. This won't work if it has a non-zero length password.

_unnest_pdf_fields

def _unnest_pdf_fields(field,
parent_name: Optional[List[str]] = None,
parent_type: Optional[str] = None,
parent_flags: Optional[int] = None) -> List[PikeField]

has_fields

def has_fields(pdf_file: str) -> bool

Check if a PDF has at least one form field using PikePDF.

Arguments

  • pdf_file str - The path to the PDF file.

Returns

  • bool - True if the PDF has at least one form field, False otherwise.

get_existing_pdf_fields

def get_existing_pdf_fields(
in_file: Union[str, Path, BinaryIO, Pdf]) -> List[List[FormField]]

Use PikePDF to get fields from the PDF

swap_pdf_page

def swap_pdf_page(*,
source_pdf: Union[str, Path, Pdf],
destination_pdf: Union[str, Path, Pdf],
source_offset: int = 0,
destination_offset: int = 0,
append_fields: bool = False,
anchor: bool = False) -> Pdf

(DEPRECATED: use copy_pdf_fields) Copies the AcroForm fields from one PDF to another blank PDF form. Optionally, choose a starting page for both the source and destination PDFs. By default, it will remove any existing annotations (which include form fields) in the destination PDF. If you wish to append annotations instead, specify append_fields = True

copy_pdf_fields

def copy_pdf_fields(*,
source_pdf: Union[str, Path, Pdf],
destination_pdf: Union[str, Path, Pdf],
source_offset: int = 0,
destination_offset: int = 0,
append_fields: bool = False,
anchor: bool = False) -> Pdf

Copies the AcroForm fields from one PDF to another blank PDF form (without AcroForm fields). Useful for getting started with an updated PDF form, where the old fields are pretty close to where they should go on the new document.

Optionally, you can choose a starting page for both the source and destination PDFs. By default, it will remove any existing annotations (which include form fields) in the destination PDF. If you wish to append annotations instead, specify append_fields = True Set anchor=True to translate field rectangles based on nearby text anchors in the destination PDF (instead of copying exact coordinates).

Example

new_pdf_with_fields = copy_pdf_fields(
source_pdf="old_pdf.pdf",
destination_pdf="new_pdf_with_no_fields.pdf")
new_pdf_with_fields.save("new_pdf_with_fields.pdf")

Arguments

  • source_pdf - a file name or path to a PDF that has AcroForm fields
  • destination_pdf - a file name or path to a PDF without AcroForm fields. Existing fields will be removed.
  • source_offset - the starting page that fields will be copied from. Defaults to 0.
  • destination_offset - the starting page that fields will be copied to. Defaults to 0.
  • append_annotations - controls whether formfyxer will try to append form fields instead of overwriting. Defaults to false; when enabled may lead to undefined behavior.
  • anchor - if true, estimate field movement per page using shared text anchors.

Returns

A pikepdf.Pdf object with new fields. If blank_pdf was a pikepdf.Pdf object, the same object is returned.

TextAnchor Objects

class TextAnchor(TypedDict)

text: str

center: XYPair

PageAnchorTransform Objects

class PageAnchorTransform(TypedDict)

scale_x: float

scale_y: float

shift_x: float

shift_y: float

matched_anchor_pairs: List[Tuple[XYPair, XYPair]]

_normalize_anchor_text

def _normalize_anchor_text(text: str) -> str

_textbox_center

def _textbox_center(bbox: BoundingBoxF) -> XYPair

_extract_unique_text_anchors_from_page

def _extract_unique_text_anchors_from_page(
page_textboxes: List[Textbox],
min_chars: int = 4) -> Dict[str, XYPair]

_page_size

def _page_size(page) -> Tuple[float, float]

_estimate_page_anchor_transform

def _estimate_page_anchor_transform(
source_page_textboxes: List[Textbox],
destination_page_textboxes: List[Textbox], source_page,
destination_page) -> Optional[PageAnchorTransform]

_transform_point

def _transform_point(point: XYPair, transform: PageAnchorTransform) -> XYPair

_local_anchor_residual_for_point

def _local_anchor_residual_for_point(point: XYPair,
transform: PageAnchorTransform,
max_distance: float = 220.0) -> XYPair

_rect_center

def _rect_center(rect: Tuple[float, float, float, float]) -> XYPair

_clamp_rect_to_page

def _clamp_rect_to_page(rect: Tuple[float, float, float, float],
page) -> Tuple[float, float, float, float]

_update_annotation_rect_from_anchor_transform

def _update_annotation_rect_from_anchor_transform(
annotation, destination_page, transform: PageAnchorTransform) -> None

_pdf_textboxes_by_page

def _pdf_textboxes_by_page(pdf: Pdf) -> List[List[Textbox]]

_get_page_anchor_transforms

def _get_page_anchor_transforms(
source_pdf: Pdf, destination_pdf: Pdf, source_offset: int,
destination_offset: int) -> List[Optional[PageAnchorTransform]]

BoxPDFPageAggregator Objects

class BoxPDFPageAggregator(PDFLayoutAnalyzer)

__init__

def __init__(rsrcmgr: PDFResourceManager,
pageno: int = 1,
laparams: Optional[LAParams] = None)

render_char

def render_char(matrix, font, fontsize, scaling, rise, cid, ncs, graphicstate)

receive_layout

def receive_layout(ltpage: LTPage) -> None

get_result

def get_result() -> List[LTPage]

BracketPDFPageAggregator Objects

class BracketPDFPageAggregator(PDFLayoutAnalyzer)

__init__

def __init__(rsrcmgr: PDFResourceManager,
pageno: int = 1,
laparams: Optional[LAParams] = None)

render_char

def render_char(matrix, font, fontsize, scaling, rise, cid, ncs, graphicstate)

receive_layout

def receive_layout(ltpage: LTPage) -> None

get_result

def get_result() -> List[LTPage]

JinjaFieldTextConverter Objects

class JinjaFieldTextConverter(TextConverter)

render_char

def render_char(matrix, font, fontsize: float, scaling: float, rise: float,
cid: int, ncs, graphicstate) -> float

PDFPageAndFieldInterpreter Objects

class PDFPageAndFieldInterpreter(PDFPageInterpreter)

__init__

def __init__(rsrcmgr: PDFResourceManager, device: PDFDevice, doc) -> None

dup

def dup() -> "PDFPageInterpreter"

get_fields_on_page

def get_fields_on_page(page_id)

process_page

def process_page(page) -> None

get_original_text_with_fields

def get_original_text_with_fields(input_file, output_file)

Gets the original text of the document, with the names of the fields in jinja format ({{field_name}})

TextAndFieldConverter Objects

class TextAndFieldConverter(TextConverter)

receive_layout

def receive_layout(ltpage: LTPage) -> None

Textbox Objects

class Textbox(TypedDict)

textbox: LTTextBoxHorizontal

bbox: BoundingBoxF

_get_nested_textboxes

def _get_nested_textboxes(obj)

get_textboxes_in_pdf

def get_textboxes_in_pdf(in_file: Union[str, Path, BinaryIO],
line_margin=0.02,
char_margin=2.0) -> List[List[Textbox]]

Gets all of the text boxes found by pdfminer in a PDF, as well as their bounding boxes

get_bracket_chars_in_pdf

def get_bracket_chars_in_pdf(in_file: Union[str, Path, BinaryIO],
line_margin=0.02,
char_margin=0.0) -> List

Gets all of the bracket characters ('[' and ']') found by pdfminer in a PDF, as well as their bounding boxes TODO: Will eventually be used to find [ ] as checkboxes, but right now we can't tell the difference between [ ] and [i]. This simply gets all of the brackets, and the characters of [hi] in a PDF and [ ] are the exact same distance apart. Currently going with just "[hi]" doesn't happen, let's hope that assumption holds.

pts_in_inch

dpi

unit_convert

def unit_convert(pix)

img2pdf_coords

def img2pdf_coords(img, max_height)

intersect_bbox

def intersect_bbox(bbox_a, bbox_b, vert_dilation=2, horiz_dilation=2) -> bool

bboxes are [left edge, bottom edge, horizontal length, vertical length]

intersect_bboxs

def intersect_bboxs(bbox_a,
bboxes,
vert_dilation=2,
horiz_dilation=2) -> Iterable[bool]

Returns an iterable of booleans, one of each of the input bboxes, true if it collides with bbox_a

contain_boxes

def contain_boxes(bbox_a: BoundingBoxF, bbox_b: BoundingBoxF) -> BoundingBoxF

Given two bounding boxes, return a single bounding box that contains both of them.

get_dist_sq

def get_dist_sq(point_a: XYPair, point_b: XYPair) -> float

returns the distance squared between two points. Faster than the true euclidean dist

get_dist

def get_dist(point_a: XYPair, point_b: XYPair) -> float

euclidean (L^2 norm) distance between two points

_default_text_field_name

def _default_text_field_name(page_index: int, field_index: int,
field_bbox: BoundingBox, font_size: int) -> str

_default_checkbox_name

def _default_checkbox_name(page_index: int, field_index: int,
field_bbox: BoundingBox) -> str

_closest_textbox

def _closest_textbox(field_bbox: BoundingBoxF,
textboxes: List[Textbox]) -> Optional[Textbox]

_sanitize_field_label

def _sanitize_field_label(text: str) -> str

_default_label_for_field

def _default_label_for_field(field_info: FormField, textboxes: List[Textbox],
used_field_names: set[str]) -> Optional[str]

_next_preferred_name

def _next_preferred_name(
preferred_names: Optional[Sequence[str]],
next_name_index: int,
used_field_names: Optional[set[str]] = None
) -> Tuple[Optional[str], int]

_is_blank_text_field

Callable[[np.ndarray, BoundingBox, int, List[Textbox]], bool] ] = None)

def _is_blank_text_field(
img_bin: np.ndarray,
bbox: BoundingBox,
line_height: int,
text_lines: List[Textbox],
is_blank_text_field: Optional[Callable[
[np.ndarray, BoundingBox, int, List[Textbox]], bool]] = None
) -> bool

get_connected_edges

def get_connected_edges(point: XYPair, point_list: Sequence)

point list is always ordered clockwise from the bottom left, i.e. bottom left, top left, top right, bottom right

bbox_distance

def bbox_distance(
bbox_a: BoundingBoxF, bbox_b: BoundingBoxF
) -> Tuple[float, Tuple[XYPair, XYPair], Tuple[XYPair, XYPair]]

Gets our specific "distance measure" between two different bounding boxes. This distance is roughly the sum of the horizontal and vertical difference in alignment of the closest shared field-bounding box edge. We are trying to find which, given a list of text boxes around a field, is the most likely to be the actual text label for the PDF field.

bboxes are 4 floats, x, y, width and height

get_possible_fields

Callable[[np.ndarray, BoundingBox, int, List[Textbox]], bool] ] = None)

def get_possible_fields(
in_pdf_file: Union[str, Path],
textboxes: Optional[List[List[Textbox]]] = None,
preferred_names: Optional[Sequence[str]] = None,
is_blank_text_field: Optional[Callable[
[np.ndarray, BoundingBox, int, List[Textbox]], bool]] = None
) -> List[List[FormField]]

Given an input PDF, runs a series of heuristics to predict where there might be places for user enterable information (i.e. PDF fields), and returns those predictions.

Example

fields = get_possible_fields('no_field.pdf')
print(fields[0][0])
# Type: FieldType.TEXT, Name: page_0_field_0, tooltip: , X: 67.68, Y: 666.0, font_size: 20, Configs: {'fieldFlags': 'doNotScroll', 'width': 239.4, 'height': 16}
# Run improve_names_with_surrounding_text(...) afterwards to derive labels from nearby text.

Arguments

  • in_pdf_file - the input PDF
  • textboxes optional - the location of various lines of text in the PDF. If not given, will be calculated automatically. This allows us to pass through expensive info to calculate through several functions.
  • preferred_names optional - field names to assign in order as fields are detected.
  • is_blank_text_field optional - override the blank-space heuristic used for detecting text entry lines.

Returns

For each page in the input PDF, a list of predicted form fields

ImproveNameVisitor Objects

class ImproveNameVisitor()

__init__

def __init__()

improve_name_with_surrounding_text

def improve_name_with_surrounding_text(field_info: FormField,
textboxes: List[Textbox]) -> FormField

AllCloseTextVisitor Objects

class AllCloseTextVisitor()

__init__

def __init__()

all_close_text

def all_close_text(field_info, textboxes) -> FormField

LowestVertVisitor Objects

class LowestVertVisitor()

Gets just the closest text to the field, and returns that

__init__

def __init__()

lowest_vert

def lowest_vert(fi: FormField, tbs: List[Textbox]) -> FormField

replace_in_original

def replace_in_original(original_text, field_map)

Given the original text of a PDF (extract_text(...)), adds the field's names in their best places. Doesn't always work, especially with duplicate text.

improve_names_with_surrounding_text

def improve_names_with_surrounding_text(
fields: List[List[FormField]],
textboxes: List[List[Textbox]],
preferred_names: Optional[Sequence[str]] = None
) -> List[List[FormField]]

surrounding_text_traverse

def surrounding_text_traverse(fields: List[List[FormField]],
textboxes: List[List[Textbox]],
visitor: Callable) -> List[List[FormField]]

get_possible_checkboxes

def get_possible_checkboxes(img: Union[str, cv2.Mat],
find_small=False) -> Union[np.ndarray, List]

Uses boxdetect library to determine if there are checkboxes on an image of a PDF page. Assumes the checkbox is square.

find_small: if true, finds smaller checkboxes. Sometimes will "find" a checkbox in letters, like O and D, if the font is too small

get_possible_radios

def get_possible_radios(img: Union[str, BinaryIO, cv2.Mat])

Even though it's called "radios", it just gets things shaped like circles, not doing any semantic analysis yet.

get_possible_text_fields

Callable[[np.ndarray, BoundingBox, int, List[Textbox]], bool] ] = None)

def get_possible_text_fields(
img: Union[str, BinaryIO, cv2.Mat],
text_lines: List[Textbox],
default_line_height: int = 44,
is_blank_text_field: Optional[Callable[
[np.ndarray, BoundingBox, int, List[Textbox]], bool]] = None
) -> List[Tuple[BoundingBox, int]]

Uses openCV to attempt to find places where a PDF could expect an input text field.

Caveats so far: only considers straight, normal horizonal lines that don't touch any vertical lines as fields Won't find field inputs as boxes

default_line_height: the default height (16 pt), in pixels (at 200 dpi), which is 45 is_blank_text_field: optional override for blank-space detection.

auto_add_fields

Callable[[np.ndarray, BoundingBox, int, List[Textbox]], bool] ] = None)

def auto_add_fields(in_pdf_file: Union[str, Path],
out_pdf_file: Union[str, Path],
preferred_names: Optional[Sequence[str]] = None,
is_blank_text_field: Optional[
Callable[[np.ndarray, BoundingBox, int, List[Textbox]],
bool]] = None)

Uses get_possible_fields and set_fields to automatically add new detected fields to an input PDF.

Example

auto_add_fields('no_fields.pdf', 'newly_added_fields.pdf')

Arguments

  • in_pdf_file - the input file name or path of the PDF where we'll try to find possible fields
  • out_pdf_file - the output file name or path of the PDF where a new version of in_pdf_file will be stored, with the new fields. Doesn't need to existing, but if a file does exist at that filename, it will be overwritten.
  • preferred_names - optional field names to assign in order.
  • is_blank_text_field - optional override for blank-space detection.

Returns

Nothing

auto_rename_fields

def auto_rename_fields(in_pdf_file: Union[str, Path],
out_pdf_file: Union[str, Path],
preferred_names: Optional[Sequence[str]] = None)

is_tagged

def is_tagged(in_pdf_file: Union[str, Path, pikepdf.Pdf]) -> bool

Determines if the input PDF file is tagged for accessibility.

Arguments

  • in_pdf_file Union[str, Path] - The path to the PDF file, as a string or a Path object.

Returns

  • bool - True if the PDF is tagged, False otherwise.