diff --git a/PyPDFForm/constants.py b/PyPDFForm/constants.py index 1546fbe5..ca37a19b 100644 --- a/PyPDFForm/constants.py +++ b/PyPDFForm/constants.py @@ -49,6 +49,7 @@ SELECTED_IDENTIFIER = "/AS" # Field flag bits +READ_ONLY = 1 << 0 MULTILINE = 1 << 12 COMB = 1 << 24 diff --git a/PyPDFForm/filler.py b/PyPDFForm/filler.py index 050cb821..0084bbbb 100644 --- a/PyPDFForm/filler.py +++ b/PyPDFForm/filler.py @@ -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, @@ -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.""" @@ -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: @@ -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) diff --git a/PyPDFForm/wrapper.py b/PyPDFForm/wrapper.py index 8e6c87e6..c4bcf0e2 100644 --- a/PyPDFForm/wrapper.py +++ b/PyPDFForm/wrapper.py @@ -45,6 +45,7 @@ def read(self) -> bytes: def fill( self, data: Dict[str, Union[str, bool, int]], + **kwargs, ) -> FormWrapper: """Fills a PDF form.""" @@ -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 @@ -179,6 +180,7 @@ def generate_coordinate_grid( def fill( self, data: Dict[str, Union[str, bool, int]], + **kwargs, ) -> PdfWrapper: """Fills a PDF form.""" diff --git a/docs/simple_fill.md b/docs/simple_fill.md index a5e352a0..84a1fed0 100644 --- a/docs/simple_fill.md +++ b/docs/simple_fill.md @@ -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. @@ -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. diff --git a/pdf_samples/simple/dropdown/dropdown_two_simple.pdf b/pdf_samples/simple/dropdown/dropdown_two_simple.pdf new file mode 100644 index 00000000..6807ad95 Binary files /dev/null and b/pdf_samples/simple/dropdown/dropdown_two_simple.pdf differ diff --git a/pdf_samples/simple/scenario/issues/521-flattened-expected.pdf b/pdf_samples/simple/scenario/issues/521-flattened-expected.pdf new file mode 100644 index 00000000..9a3053ae Binary files /dev/null and b/pdf_samples/simple/scenario/issues/521-flattened-expected.pdf differ diff --git a/tests/scenario/test_issues_simple.py b/tests/scenario/test_issues_simple.py index 84354d07..1710d5bc 100644 --- a/tests/scenario/test_issues_simple.py +++ b/tests/scenario/test_issues_simple.py @@ -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 diff --git a/tests/test_dropdown_simple.py b/tests/test_dropdown_simple.py index b1efcf89..bc8b5d23 100644 --- a/tests/test_dropdown_simple.py +++ b/tests/test_dropdown_simple.py @@ -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"