Skip to content

Commit

Permalink
Merge pull request #529 from chinapandaman/PPF-528
Browse files Browse the repository at this point in the history
PPF-528: implement flatten option for form wrapper
  • Loading branch information
chinapandaman authored Mar 19, 2024
2 parents e7b39b6 + 513e694 commit 30420dd
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 6 deletions.
1 change: 1 addition & 0 deletions PyPDFForm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
SELECTED_IDENTIFIER = "/AS"

# Field flag bits
READ_ONLY = 1 << 0
MULTILINE = 1 << 12
COMB = 1 << 24

Expand Down
27 changes: 24 additions & 3 deletions PyPDFForm/filler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from typing import Dict, cast

from pypdf import PdfReader, PdfWriter
from pypdf.generic import DictionaryObject, NameObject, TextStringObject
from pypdf.generic import (DictionaryObject, NameObject, NumberObject,
TextStringObject)

from .constants import (ANNOTATION_KEY, CHECKBOX_SELECT, SELECTED_IDENTIFIER,
from .constants import (ANNOTATION_KEY, CHECKBOX_SELECT, FIELD_FLAG_KEY,
PARENT_KEY, READ_ONLY, SELECTED_IDENTIFIER,
TEXT_VALUE_IDENTIFIER, TEXT_VALUE_SHOW_UP_IDENTIFIER,
WIDGET_TYPES)
from .coordinate import (get_draw_checkbox_radio_coordinates,
Expand Down Expand Up @@ -130,6 +132,7 @@ def fill(
def simple_fill(
template: bytes,
widgets: Dict[str, WIDGET_TYPES],
flatten: bool = False,
) -> bytes:
"""Fills a PDF form in place."""

Expand All @@ -148,7 +151,7 @@ def simple_fill(
if widget is None:
continue

if isinstance(widget, Checkbox) and widget.value is True:
if type(widget) is Checkbox and widget.value is True:
annot[NameObject(SELECTED_IDENTIFIER)] = NameObject(CHECKBOX_SELECT)
elif isinstance(widget, Radio):
if key not in radio_button_tracker:
Expand All @@ -173,6 +176,24 @@ def simple_fill(
widget.value
)

if flatten:
if isinstance(widget, Radio):
annot[NameObject(PARENT_KEY)][ # noqa
NameObject(FIELD_FLAG_KEY)
] = NumberObject(
int(
annot[NameObject(PARENT_KEY)].get( # noqa
NameObject(FIELD_FLAG_KEY), 0
)
)
| READ_ONLY
)
else:
annot[NameObject(FIELD_FLAG_KEY)] = NumberObject(
int(annot.get(NameObject(FIELD_FLAG_KEY), 0))
| READ_ONLY # noqa
)

with BytesIO() as f:
out.write(f)
f.seek(0)
Expand Down
4 changes: 3 additions & 1 deletion PyPDFForm/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def read(self) -> bytes:
def fill(
self,
data: Dict[str, Union[str, bool, int]],
**kwargs,
) -> FormWrapper:
"""Fills a PDF form."""

Expand All @@ -54,7 +55,7 @@ def fill(
if key in widgets:
widgets[key].value = value

self.stream = simple_fill(self.read(), widgets)
self.stream = simple_fill(self.read(), widgets, flatten=kwargs.get("flatten", False))

return self

Expand Down Expand Up @@ -179,6 +180,7 @@ def generate_coordinate_grid(
def fill(
self,
data: Dict[str, Union[str, bool, int]],
**kwargs,
) -> PdfWrapper:
"""Fills a PDF form."""

Expand Down
7 changes: 5 additions & 2 deletions docs/simple_fill.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
**NOTE:** This page contains beta features, meaning it's known that these features do not support some PDF forms but
currently there are no plans and/or solutions to fix them.

The `FormWrapper` class allows you to fill a PDF form in place as if you were filling it manually. The resulted filled
PDF form will NOT be flattened and will still be editable.
The `FormWrapper` class allows you to fill a PDF form in place as if you were filling it manually.

Similar to the `PdfWrapper` class, the `FormWrapper` also supports widgets including text fields, checkboxes, radio
buttons, dropdowns, and paragraphs. However, it does NOT support signature widgets.
Expand All @@ -25,8 +24,12 @@ filled = FormWrapper("sample_template_with_dropdown.pdf").fill(
"radio_1": 1,
"dropdown_1": 1,
},
flatten=False,
)

with open("output.pdf", "wb+") as output:
output.write(filled.read())
```

The optional parameter `flatten` has a default value of `False`, meaning PDF forms filled using `FormWrapper` will by
default remain editable. Setting it to `True` will flatten the PDF after it's filled, making all widgets read only.
Binary file not shown.
Binary file not shown.
23 changes: 23 additions & 0 deletions tests/scenario/test_issues_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,26 @@ def test_521(issue_pdf_directory, pdf_samples, request):

assert len(obj.read()) == len(expected)
assert obj.stream == expected


def test_521_flattened(issue_pdf_directory, pdf_samples, request):
expected_path = os.path.join(
pdf_samples, "simple", "scenario", "issues", "521-flattened-expected.pdf"
)
with open(expected_path, "rb+") as f:
obj = FormWrapper(os.path.join(issue_pdf_directory, "521.pdf")).fill(
{
"Text1": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?", # noqa
"Text2": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. NEMO ENIM IPSAM VOLUPTATEM QUIA VOLUPTAS SIT ASPERNATUR AUT ODIT AUT FUGIT, SED QUIA CONSEQUUNTUR MAGNI DOLORES EOS QUI RATIONE VOLUPTATEM SEQUI NESCIUNT. NEQUE PORRO QUISQUAM EST, QUI DOLOREM IPSUM QUIA DOLOR SIT AMET, CONSECTETUR, ADIPISCI VELIT, SED QUIA NON NUMQUAM EIUS MODI TEMPORA INCIDUNT UT LABORE ET DOLORE MAGNAM ALIQUAM QUAERAT VOLUPTATEM. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?", # noqa
"Text3": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?", # noqa
},
flatten=True,
)

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

expected = f.read()

assert len(obj.read()) == len(expected)
assert obj.stream == expected
26 changes: 26 additions & 0 deletions tests/test_dropdown_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@ def test_dropdown_two(sample_template_with_dropdown, pdf_samples, request):
assert obj.stream == expected


def test_dropdown_two_simple(sample_template_with_dropdown, pdf_samples, request):
expected_path = os.path.join(pdf_samples, "simple", "dropdown", "dropdown_two_simple.pdf")
with open(expected_path, "rb+") as f:
obj = FormWrapper(sample_template_with_dropdown).fill(
{
"test_1": "test_1",
"test_2": "test_2",
"test_3": "test_3",
"check_1": True,
"check_2": True,
"check_3": True,
"radio_1": 1,
"dropdown_1": 1,
},
flatten=True,
)

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

expected = f.read()

assert len(obj.read()) == len(expected)
assert obj.stream == expected


def test_dropdown_three(sample_template_with_dropdown, pdf_samples, request):
expected_path = os.path.join(
pdf_samples, "simple", "dropdown", "dropdown_three.pdf"
Expand Down

0 comments on commit 30420dd

Please sign in to comment.