Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PPF-606: resolve R0912 #616

Merged
merged 10 commits into from
May 8, 2024
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[MESSAGES CONTROL]
disable=C0103, R0912, R0913, R0902, R0903, R0914, C0209, C0123
disable=C0103, R0913, R0902, R0903, R0914, C0209, C0123
166 changes: 104 additions & 62 deletions PyPDFForm/filler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""Contains helpers for filling a PDF form."""

from io import BytesIO
from typing import Dict, cast
from typing import Dict, cast, Union, Tuple

from pypdf import PdfReader, PdfWriter
from pypdf.generic import DictionaryObject
Expand All @@ -29,6 +29,95 @@
from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf


def check_radio_handler(
widget: dict,
middleware: Union[Checkbox, Radio],
radio_button_tracker: dict
) -> Tuple[
Text, Union[float, int], Union[float, int], bool
]:
"""Handles draw parameters for checkbox and radio button widgets."""

font_size = (
checkbox_radio_font_size(widget)
if middleware.size is None
else middleware.size
)
to_draw = checkbox_radio_to_draw(middleware, font_size)
x, y = get_draw_checkbox_radio_coordinates(widget, to_draw)
text_needs_to_be_drawn = False
if type(middleware) is Checkbox and middleware.value:
text_needs_to_be_drawn = True
elif isinstance(middleware, Radio):
if middleware.name not in radio_button_tracker:
radio_button_tracker[middleware.name] = 0
radio_button_tracker[middleware.name] += 1
if middleware.value == radio_button_tracker[middleware.name] - 1:
text_needs_to_be_drawn = True

return to_draw, x, y, text_needs_to_be_drawn


def signature_image_handler(
widget: dict,
middleware: Union[Signature, Image],
images_to_draw: list
) -> bool:
"""Handles draw parameters for signature and image widgets."""

stream = middleware.stream
any_image_to_draw = False
if stream is not None:
any_image_to_draw = True
stream = any_image_to_jpg(stream)
x, y, width, height = get_draw_image_coordinates_resolutions(
widget
)
images_to_draw.append(
[
stream,
x,
y,
width,
height,
]
)

return any_image_to_draw


def text_handler(
widget: dict,
middleware: Text
) -> Tuple[
Text, Union[float, int], Union[float, int], bool
]:
"""Handles draw parameters for text field widgets."""

middleware.text_line_x_coordinates = get_text_line_x_coordinates(
widget, middleware
)
x, y = get_draw_text_coordinates(widget, middleware)
to_draw = middleware
text_needs_to_be_drawn = True

return to_draw, x, y, text_needs_to_be_drawn


def get_drawn_stream(to_draw: dict, stream: bytes, action: str) -> bytes:
"""Generates a stream of an input PDF stream with stuff drawn on it."""

watermark_list = []
for page, stuffs in to_draw.items():
watermark_list.append(b"")
watermarks = create_watermarks_and_draw(stream, page, action, stuffs)
for i, watermark in enumerate(watermarks):
if watermark:
watermark_list[i] = watermark

return merge_watermarks_with_pdf(stream, watermark_list)


def fill(
template_stream: bytes,
widgets: Dict[str, WIDGET_TYPES],
Expand All @@ -38,95 +127,48 @@ def fill(
texts_to_draw = {}
images_to_draw = {}
any_image_to_draw = False
text_watermarks = []
image_watermarks = []

radio_button_tracker = {}

for page, _widgets in get_widgets_by_page(template_stream).items():
for page, widget_dicts in get_widgets_by_page(template_stream).items():
texts_to_draw[page] = []
images_to_draw[page] = []
text_watermarks.append(b"")
image_watermarks.append(b"")
for _widget in _widgets:
key = get_widget_key(_widget)
for widget_dict in widget_dicts:
key = get_widget_key(widget_dict)
text_needs_to_be_drawn = False
_to_draw = x = y = None
to_draw = x = y = None

if isinstance(widgets[key], (Checkbox, Radio)):
font_size = (
checkbox_radio_font_size(_widget)
if widgets[key].size is None
else widgets[key].size
to_draw, x, y, text_needs_to_be_drawn = check_radio_handler(
widget_dict, widgets[key], radio_button_tracker
)
_to_draw = checkbox_radio_to_draw(widgets[key], font_size)
x, y = get_draw_checkbox_radio_coordinates(_widget, _to_draw)
if type(widgets[key]) is Checkbox and widgets[key].value:
text_needs_to_be_drawn = True
elif isinstance(widgets[key], Radio):
if key not in radio_button_tracker:
radio_button_tracker[key] = 0
radio_button_tracker[key] += 1
if widgets[key].value == radio_button_tracker[key] - 1:
text_needs_to_be_drawn = True
elif isinstance(widgets[key], (Signature, Image)):
stream = widgets[key].stream
if stream is not None:
any_image_to_draw = True
stream = any_image_to_jpg(stream)
x, y, width, height = get_draw_image_coordinates_resolutions(
_widget
)
images_to_draw[page].append(
[
stream,
x,
y,
width,
height,
]
)
else:
widgets[key].text_line_x_coordinates = get_text_line_x_coordinates(
_widget, widgets[key]
any_image_to_draw = signature_image_handler(
widget_dict, widgets[key], images_to_draw[page]
)
x, y = get_draw_text_coordinates(_widget, widgets[key])
_to_draw = widgets[key]
text_needs_to_be_drawn = True
else:
to_draw, x, y, text_needs_to_be_drawn = text_handler(widget_dict, widgets[key])

if all(
[
text_needs_to_be_drawn,
_to_draw is not None,
to_draw is not None,
x is not None,
y is not None,
]
):
texts_to_draw[page].append(
[
_to_draw,
to_draw,
x,
y,
]
)

for page, texts in texts_to_draw.items():
_watermarks = create_watermarks_and_draw(template_stream, page, "text", texts)
for i, watermark in enumerate(_watermarks):
if watermark:
text_watermarks[i] = watermark

result = merge_watermarks_with_pdf(template_stream, text_watermarks)
result = get_drawn_stream(texts_to_draw, template_stream, "text")

if any_image_to_draw:
for page, images in images_to_draw.items():
_watermarks = create_watermarks_and_draw(
template_stream, page, "image", images
)
for i, watermark in enumerate(_watermarks):
if watermark:
image_watermarks[i] = watermark
result = merge_watermarks_with_pdf(result, image_watermarks)
result = get_drawn_stream(images_to_draw, result, "image")

return result

Expand Down
39 changes: 24 additions & 15 deletions PyPDFForm/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,11 @@ def register_font(font_name: str, ttf_stream: bytes) -> bool:
return result


def auto_detect_font(widget: dict) -> str:
"""Returns the font of the text field if it is one of the standard fonts."""

result = DEFAULT_FONT

text_appearance = None
for pattern in TEXT_FIELD_APPEARANCE_PATTERNS:
text_appearance = traverse_pattern(pattern, widget)

if text_appearance:
break

if not text_appearance:
return result
def extract_font_from_text_appearance(text_appearance: str) -> Union[str, None]:
"""
Uses regex to pattern match out the font from the text
appearance string of a text field widget.
"""

text_appearance = text_appearance.split(" ")

Expand All @@ -72,7 +63,25 @@ def auto_detect_font(widget: dict) -> str:
if found:
return font

return result
return None


def auto_detect_font(widget: dict) -> str:
"""Returns the font of the text field if it is one of the standard fonts."""

result = DEFAULT_FONT

text_appearance = None
for pattern in TEXT_FIELD_APPEARANCE_PATTERNS:
text_appearance = traverse_pattern(pattern, widget)

if text_appearance:
break

if not text_appearance:
return result

return extract_font_from_text_appearance(text_appearance) or result


def text_field_font_size(widget: dict) -> Union[float, int]:
Expand Down
55 changes: 41 additions & 14 deletions PyPDFForm/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,26 +290,24 @@ def get_character_x_paddings(widget: dict, widget_middleware: Text) -> List[floa
return result


def get_paragraph_lines(widget: dict, widget_middleware: Text) -> List[str]:
"""Splits the paragraph field's text to a list of lines."""
def split_characters_into_lines(
split_by_new_line_symbol: List[str], middleware: Text, width: float
) -> List[str]:
"""
Given a long string meant to be filled for a paragraph widget
split by the new line symbol already, splits it further into lines
where each line would fit into the widget's width.
"""

lines = []
result = []
value = widget_middleware.value or ""
if widget_middleware.max_length is not None:
value = value[: widget_middleware.max_length]

width = abs(float(widget[Rect][0]) - float(widget[Rect][2]))

split_by_new_line_symbol = value.split(NEW_LINE_SYMBOL)
for line in split_by_new_line_symbol:
characters = line.split(" ")
current_line = ""
for each in characters:
line_extended = f"{current_line} {each}" if current_line else each
if (
stringWidth(
line_extended, widget_middleware.font, widget_middleware.font_size
line_extended, middleware.font, middleware.font_size
)
<= width
):
Expand All @@ -323,12 +321,26 @@ def get_paragraph_lines(widget: dict, widget_middleware: Text) -> List[str]:
else current_line
)

return lines


def adjust_each_line(
lines: List[str], middleware: Text, width: float
) -> List[str]:
"""
Given a list of strings which is the return value of
`split_characters_into_lines`, further adjusts each line
so that there is neither overflow nor over-splitting into
unnecessary lines.
"""

result = []
for each in lines:
tracker = ""
for char in each:
check = tracker + char
if (
stringWidth(check, widget_middleware.font, widget_middleware.font_size)
stringWidth(check, middleware.font, middleware.font_size)
> width
):
result.append(tracker)
Expand All @@ -342,8 +354,8 @@ def get_paragraph_lines(widget: dict, widget_middleware: Text) -> List[str]:
result
and stringWidth(
f"{each} {result[-1]}",
widget_middleware.font,
widget_middleware.font_size,
middleware.font,
middleware.font_size,
)
<= width
and NEW_LINE_SYMBOL not in result[-1]
Expand All @@ -361,6 +373,21 @@ def get_paragraph_lines(widget: dict, widget_middleware: Text) -> List[str]:
return result


def get_paragraph_lines(widget: dict, widget_middleware: Text) -> List[str]:
"""Splits the paragraph field's text to a list of lines."""

value = widget_middleware.value or ""
if widget_middleware.max_length is not None:
value = value[: widget_middleware.max_length]

width = abs(float(widget[Rect][0]) - float(widget[Rect][2]))

split_by_new_line_symbol = value.split(NEW_LINE_SYMBOL)
lines = split_characters_into_lines(split_by_new_line_symbol, widget_middleware, width)

return adjust_each_line(lines, widget_middleware, width)


def get_paragraph_auto_wrap_length(widget_middleware: Text) -> int:
"""Calculates the text wrap length of a paragraph field."""

Expand Down
Loading