Skip to content

Commit

Permalink
Merge pull request #504 from chinapandaman/PPF-502
Browse files Browse the repository at this point in the history
PPF-502: supports signature field
  • Loading branch information
chinapandaman authored Feb 16, 2024
2 parents 40813e0 + ddfb172 commit 6f6c1f9
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 11 deletions.
1 change: 1 addition & 0 deletions PyPDFForm/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
PARENT_KEY = "/Parent"
FIELD_FLAG_KEY = "/Ff"
TEXT_FIELD_IDENTIFIER = "/Tx"
SIGNATURE_FIELD_IDENTIFIER = "/Sig"
TEXT_FIELD_APPEARANCE_IDENTIFIER = "/DA"
SELECTABLE_IDENTIFIER = "/Btn"
TEXT_FIELD_MAX_LENGTH_KEY = "/MaxLen"
Expand Down
19 changes: 19 additions & 0 deletions PyPDFForm/core/coordinate.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ def get_draw_checkbox_radio_coordinates(
)


def get_draw_sig_coordinates_resolutions(widget: dict) -> Tuple[float, float, float, float]:
"""
Returns coordinates and resolutions to draw signature at given a PDF form signature widget.
"""

x = float(widget[ANNOTATION_RECTANGLE_KEY][0])
y = float(widget[ANNOTATION_RECTANGLE_KEY][1])
width = abs(
float(widget[ANNOTATION_RECTANGLE_KEY][0])
- float(widget[ANNOTATION_RECTANGLE_KEY][2])
)
height = abs(
float(widget[ANNOTATION_RECTANGLE_KEY][1])
- float(widget[ANNOTATION_RECTANGLE_KEY][3])
)

return x, y, width, height


def get_draw_text_coordinates(
widget: dict, widget_middleware: Text
) -> Tuple[Union[float, int], Union[float, int]]:
Expand Down
49 changes: 42 additions & 7 deletions PyPDFForm/core/filler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
from ..middleware.checkbox import Checkbox
from ..middleware.constants import WIDGET_TYPES
from ..middleware.radio import Radio
from ..middleware.signature import Signature
from .coordinate import (get_draw_checkbox_radio_coordinates,
get_draw_text_coordinates,
get_text_line_x_coordinates)
get_text_line_x_coordinates,
get_draw_sig_coordinates_resolutions)
from .font import checkbox_radio_font_size
from .image import any_image_to_jpg
from .template import get_widget_key, get_widgets_by_page
from .utils import checkbox_radio_to_draw
from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
Expand All @@ -21,39 +24,61 @@ def fill(
) -> bytes:
"""Fills a PDF using watermarks."""

# pylint: disable=too-many-branches
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():
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)
needs_to_be_drawn = False
text_needs_to_be_drawn = False

if isinstance(widgets[key], (Checkbox, Radio)):
font_size = checkbox_radio_font_size(_widget)
_to_draw = checkbox_radio_to_draw(widgets[key], font_size)
x, y = get_draw_checkbox_radio_coordinates(_widget, _to_draw)
if isinstance(widgets[key], Checkbox) and widgets[key].value:
needs_to_be_drawn = True
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:
needs_to_be_drawn = True
text_needs_to_be_drawn = True
elif isinstance(widgets[key], Signature):
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_sig_coordinates_resolutions(_widget)
images_to_draw[page].append(
[
stream,
x,
y,
width,
height,
]
)
text_needs_to_be_drawn = False
else:
widgets[key].text_line_x_coordinates = get_text_line_x_coordinates(
_widget, widgets[key]
)
x, y = get_draw_text_coordinates(_widget, widgets[key])
_to_draw = widgets[key]
needs_to_be_drawn = True
text_needs_to_be_drawn = True

if needs_to_be_drawn:
if text_needs_to_be_drawn:
texts_to_draw[page].append(
[
_to_draw,
Expand All @@ -68,4 +93,14 @@ def fill(
if watermark:
text_watermarks[i] = watermark

return merge_watermarks_with_pdf(template_stream, text_watermarks)
result = merge_watermarks_with_pdf(template_stream, text_watermarks)

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)

return result
7 changes: 6 additions & 1 deletion PyPDFForm/core/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@
from ..middleware.dropdown import Dropdown
from ..middleware.radio import Radio
from ..middleware.text import Text
from ..middleware.signature import Signature
from .constants import (ANNOTATION_FIELD_KEY, BUTTON_IDENTIFIER,
BUTTON_STYLE_IDENTIFIER, CHOICE_FIELD_IDENTIFIER,
CHOICES_IDENTIFIER, FIELD_FLAG_KEY, PARENT_KEY,
SELECTABLE_IDENTIFIER, SUBTYPE_KEY,
TEXT_FIELD_ALIGNMENT_IDENTIFIER,
TEXT_FIELD_APPEARANCE_IDENTIFIER,
TEXT_FIELD_IDENTIFIER, WIDGET_SUBTYPE_KEY,
WIDGET_TYPE_KEY)
WIDGET_TYPE_KEY, SIGNATURE_FIELD_IDENTIFIER)

WIDGET_TYPE_PATTERNS = [
(
({WIDGET_TYPE_KEY: SIGNATURE_FIELD_IDENTIFIER},),
Signature,
),
(
({WIDGET_TYPE_KEY: TEXT_FIELD_IDENTIFIER},),
Text,
Expand Down
3 changes: 2 additions & 1 deletion PyPDFForm/middleware/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .dropdown import Dropdown
from .radio import Radio
from .text import Text
from .signature import Signature

VERSION_IDENTIFIERS = [
b"%PDF-1.0",
Expand All @@ -21,6 +22,6 @@
]
VERSION_IDENTIFIER_PREFIX = b"%PDF-"

WIDGET_TYPES = Union[Text, Checkbox, Radio, Dropdown]
WIDGET_TYPES = Union[Text, Checkbox, Radio, Dropdown, Signature]

DEPRECATION_NOTICE = "{} will be deprecated soon. Use {} instead."
43 changes: 43 additions & 0 deletions PyPDFForm/middleware/signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""Contains signature middleware."""

from typing import BinaryIO, Union
from os.path import expanduser

from .adapter import fp_or_f_obj_or_stream_to_stream
from .widget import Widget


class Signature(Widget):
"""A class to represent a signature field widget."""

def __init__(
self,
name: str,
value: Union[bytes, str, BinaryIO] = None,
) -> None:
"""Constructs all attributes for the signature field."""

super().__init__(name, value)

@property
def schema_definition(self) -> dict:
"""Json schema definition of the signature field."""

return {"type": "string"}

@property
def sample_value(self) -> str:
"""Sample value of the signature field."""

return expanduser("~/Downloads/sample_image.jpg")

@property
def stream(self) -> Union[bytes, None]:
"""Converts the value of the signature field image to a stream."""

return (
fp_or_f_obj_or_stream_to_stream(self.value)
if self.value is not None
else None
)
4 changes: 2 additions & 2 deletions PyPDFForm/middleware/widget.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Contains widget middleware."""

from typing import Any, Union
from typing import Any


class Widget:
Expand All @@ -10,7 +10,7 @@ class Widget:
def __init__(
self,
name: str,
value: Union[str, bool, int] = None,
value: Any = None,
) -> None:
"""Constructs basic attributes for the object."""

Expand Down
Binary file added image_samples/sample_signature.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file added pdf_samples/signature/test_fill_signature.pdf
Binary file not shown.
Binary file not shown.
60 changes: 60 additions & 0 deletions tests/test_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-

import os

from PyPDFForm import PdfWrapper


def test_fill_signature(pdf_samples, image_samples, request):
expected_path = os.path.join(
pdf_samples, "signature", "test_fill_signature.pdf"
)
with open(expected_path, "rb+") as f:
obj = PdfWrapper(
os.path.join(pdf_samples, "signature", "sample_template_with_signature.pdf")
).fill(
{"signature": os.path.join(image_samples, "sample_signature.png")}
)

request.config.results["expected_path"] = expected_path
request.config.results["stream"] = obj.read()

expected = f.read()

if os.name != "nt":
assert len(obj.read()) == len(expected)
assert obj.read() == expected


def test_signature_schema(pdf_samples):
obj = PdfWrapper(os.path.join(pdf_samples, "signature", "sample_template_with_signature.pdf"))

assert obj.widgets["signature"].schema_definition == {"type": "string"}


def test_signature_sample_value(pdf_samples):
obj = PdfWrapper(os.path.join(pdf_samples, "signature", "sample_template_with_signature.pdf"))

assert obj.widgets["signature"].sample_value == os.path.expanduser(
"~/Downloads/sample_image.jpg")


def test_fill_signature_overlap(pdf_samples, image_samples, request):
expected_path = os.path.join(
pdf_samples, "signature", "test_fill_signature_overlap.pdf"
)
with open(expected_path, "rb+") as f:
obj = PdfWrapper(
os.path.join(pdf_samples, "signature", "sample_template_with_signature_overlap.pdf")
).fill(
{"signature": os.path.join(image_samples, "sample_signature.png")}
)

request.config.results["expected_path"] = expected_path
request.config.results["stream"] = obj.read()

expected = f.read()

if os.name != "nt":
assert len(obj.read()) == len(expected)
assert obj.read() == expected

0 comments on commit 6f6c1f9

Please sign in to comment.