Skip to content

Commit

Permalink
Merge pull request #272 from chinapandaman/PPF-271
Browse files Browse the repository at this point in the history
PPF-271: support max length and combed text fields
  • Loading branch information
chinapandaman authored Oct 31, 2022
2 parents 9a2fbc0 + 75ff7b8 commit cd09827
Show file tree
Hide file tree
Showing 20 changed files with 289 additions and 30 deletions.
2 changes: 1 addition & 1 deletion PyPDFForm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
PyPDFForm = Wrapper
PyPDFForm2 = WrapperV2

__version__ = "1.0.3"
__version__ = "1.0.4"
10 changes: 8 additions & 2 deletions PyPDFForm/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ def parent_key(self) -> str:
return "/Parent"

@property
def field_editable_key(self) -> str:
"""Used for identifying if an element is still editable for pdfrw parsed PDF form."""
def field_flag_key(self) -> str:
"""Field flags specific to text fields."""

return "/Ff"

Expand All @@ -77,6 +77,12 @@ def selectable_identifier(self) -> str:

return "/Btn"

@property
def text_field_max_length_key(self) -> str:
"""Used for identifying a text field's max number of characters allowed."""

return "/MaxLen"


class Merge:
"""Contains constants for merging PDFs."""
Expand Down
23 changes: 17 additions & 6 deletions PyPDFForm/core/filler.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def fill(
key = TemplateCore().get_element_key(_element, sejda)

update_dict = {
TemplateConstants().field_editable_key.replace(
TemplateConstants().field_flag_key.replace(
"/", ""
): pdfrw.PdfObject(1)
}
Expand Down Expand Up @@ -98,7 +98,7 @@ def fill(
_element[TemplateConstants().parent_key].update(
pdfrw.PdfDict(
**{
TemplateConstants().field_editable_key.replace(
TemplateConstants().field_flag_key.replace(
"/", ""
): pdfrw.PdfObject(1)
}
Expand Down Expand Up @@ -181,7 +181,7 @@ def simple_fill(
element[TemplateConstants().parent_key].update(
pdfrw.PdfDict(
**{
TemplateConstants().field_editable_key.replace(
TemplateConstants().field_flag_key.replace(
"/", ""
): pdfrw.PdfObject(1)
}
Expand All @@ -197,7 +197,7 @@ def simple_fill(

if not editable:
update_dict[
TemplateConstants().field_editable_key.replace("/", "")
TemplateConstants().field_flag_key.replace("/", "")
] = pdfrw.PdfObject(1)

element.update(pdfrw.PdfDict(**update_dict))
Expand Down Expand Up @@ -263,11 +263,22 @@ def fill_v2(
]
)
else:
if elements[key].max_length:
x, y = TemplateCore().get_draw_text_with_max_length_coordinates(
_element,
elements[key].font_size,
len(elements[key].value),
elements[key].comb,
elements[key].max_length,
)
else:
x, y = TemplateCore().get_draw_text_coordinates(_element)

texts_to_draw[page].append(
[
elements[key],
TemplateCore().get_draw_text_coordinates(_element)[0],
TemplateCore().get_draw_text_coordinates(_element)[1],
x,
y,
]
)

Expand Down
66 changes: 66 additions & 0 deletions PyPDFForm/core/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .constants import Template as TemplateCoreConstants
from .patterns import ELEMENT_KEY_PATTERNS, ELEMENT_TYPE_PATTERNS
from .utils import Utils
from math import sqrt


class Template:
Expand Down Expand Up @@ -307,6 +308,25 @@ def get_draw_text_coordinates(
- 2,
)

@staticmethod
def get_text_field_max_length(element: "pdfrw.PdfDict") -> Union[int, None]:
"""Returns the max length of the text field if presented or None."""

return (
int(element[TemplateCoreConstants().text_field_max_length_key])
if TemplateCoreConstants().text_field_max_length_key in element
else None
)

@staticmethod
def is_text_field_comb(element: "pdfrw.PdfDict") -> bool:
"""Returns true if characters in a text field needs to be formatted into combs."""

try:
return "{0:b}".format(int(element["/Ff"]))[::-1][24] == "1"
except IndexError:
return False

def assign_uuid(self, pdf: bytes) -> bytes:
"""Appends a separator and uuid after each element's annotated name."""

Expand All @@ -330,3 +350,49 @@ def assign_uuid(self, pdf: bytes) -> bytes:
element.update(pdfrw.PdfDict(**update_dict))

return Utils().generate_stream(pdf_file)

@staticmethod
def font_size_for_text_field_with_max_length(
element: "pdfrw.PdfDict",
max_length: int,
) -> float:
"""Calculates the font size for a text field with max length."""

area = abs(
float(element[TemplateCoreConstants().annotation_rectangle_key][0])
- float(element[TemplateCoreConstants().annotation_rectangle_key][2])
) * abs(
float(element[TemplateCoreConstants().annotation_rectangle_key][1])
- float(element[TemplateCoreConstants().annotation_rectangle_key][3])
)

return sqrt(area / max_length)

@staticmethod
def get_draw_text_with_max_length_coordinates(
element: "pdfrw.PdfDict",
font_size: Union[float, int],
length: int,
comb: bool,
max_length: int,
) -> Tuple[Union[float, int], Union[float, int]]:
"""Returns coordinates to draw at given a PDF form text field with max length."""

length = min(length, max_length)
width = font_size * length * 96 / 72
height = font_size * 96 / 72
c_half_width = (
float(element[TemplateCoreConstants().annotation_rectangle_key][0])
+ float(element[TemplateCoreConstants().annotation_rectangle_key][2])
) / 2
c_half_height = (
float(element[TemplateCoreConstants().annotation_rectangle_key][1])
+ float(element[TemplateCoreConstants().annotation_rectangle_key][3])
) / 2

return (
(c_half_width - width / 2 + c_half_width) / 2 - (
font_size * 0.5 * 96 / 72 if (comb is True and length % 2 == 0) else 0
),
(c_half_height - height / 2 + c_half_height) / 2,
)
19 changes: 12 additions & 7 deletions PyPDFForm/core/watermark.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,32 +32,37 @@ def draw_text(
coordinate_x = args[2]
coordinate_y = args[3]

if not element.value:
element.value = ""
text_to_draw = element.value

if not text_to_draw:
text_to_draw = ""

if element.max_length is not None:
text_to_draw = text_to_draw[:element.max_length]

canv.setFont(element.font, element.font_size)
canv.setFillColorRGB(
element.font_color[0], element.font_color[1], element.font_color[2]
)

if len(element.value) < element.text_wrap_length:
if len(text_to_draw) < element.text_wrap_length:
canv.drawString(
coordinate_x + element.text_x_offset,
coordinate_y + element.text_y_offset,
element.value,
text_to_draw,
)
else:
text_obj = canv.beginText(0, 0)

start = 0
end = element.text_wrap_length

while end < len(element.value):
text_obj.textLine(element.value[start:end])
while end < len(text_to_draw):
text_obj.textLine(text_to_draw[start:end])
start += element.text_wrap_length
end += element.text_wrap_length

text_obj.textLine(element.value[start:])
text_obj.textLine(text_to_draw[start:])

canv.saveState()
canv.translate(
Expand Down
4 changes: 3 additions & 1 deletion PyPDFForm/middleware/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


class ElementType(Enum):
"""A enum to represent types of elements."""
"""An enum to represent types of elements."""

text = "text"
checkbox = "checkbox"
Expand Down Expand Up @@ -43,6 +43,8 @@ def __init__(
self.text_x_offset = None
self.text_y_offset = None
self.text_wrap_length = None
self.max_length = None
self.comb = None

@property
def name(self) -> str:
Expand Down
17 changes: 15 additions & 2 deletions PyPDFForm/middleware/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Dict, Union

from ..core.template import Template as TemplateCore
from .element import Element
from .element import Element, ElementType
from .exceptions.template import InvalidTemplateError


Expand Down Expand Up @@ -54,10 +54,23 @@ def build_elements_v2(pdf_stream: bytes) -> Dict[str, "Element"]:
key = TemplateCore().get_element_key_v2(element)

element_type = TemplateCore().get_element_type_v2(element)

if element_type is not None:
results[key] = Element(
_element = Element(
element_name=key,
element_type=element_type,
)

if _element.type == ElementType.text:
_element.max_length = TemplateCore().get_text_field_max_length(element)
if _element.max_length is not None and TemplateCore().is_text_field_comb(element):
_element.comb = True

if _element.max_length is not None:
_element.font_size = TemplateCore().font_size_for_text_field_with_max_length(
element, _element.max_length
)

results[key] = _element

return results
7 changes: 4 additions & 3 deletions PyPDFForm/middleware/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,10 @@ def __init__(
for each in self.elements.values():
if each.type == ElementType.text:
each.font = kwargs.get("global_font", TextConstants().global_font)
each.font_size = kwargs.get(
"global_font_size", TextConstants().global_font_size
)
if each.max_length is None:
each.font_size = kwargs.get(
"global_font_size", TextConstants().global_font_size
)
each.font_color = kwargs.get(
"global_font_color", TextConstants().global_font_color
)
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ def sejda_template(pdf_samples):
return f.read()


@pytest.fixture
def sample_template_with_max_length_text_field(pdf_samples):
with open(os.path.join(pdf_samples, "sample_template_with_max_length_text_field.pdf"), "rb+") as f:
return f.read()


@pytest.fixture
def sample_template_with_comb_text_field(pdf_samples):
with open(os.path.join(pdf_samples, "sample_template_with_comb_text_field.pdf"), "rb+") as f:
return f.read()


@pytest.fixture
def sejda_data():
return {
Expand Down
Loading

0 comments on commit cd09827

Please sign in to comment.