diff --git a/PyPDFForm/constants.py b/PyPDFForm/constants.py index 1b39dd25..7c00ffde 100644 --- a/PyPDFForm/constants.py +++ b/PyPDFForm/constants.py @@ -27,6 +27,8 @@ DEPRECATION_NOTICE = "{} will be deprecated soon. Use {} instead." Annots = "/Annots" +A = "/A" +JS = "/JS" T = "/T" Rect = "/Rect" Subtype = "/Subtype" @@ -64,6 +66,8 @@ NEW_LINE_SYMBOL = "\n" +IMAGE_FIELD_IDENTIFIER = "event.target.buttonImportIcon();" + DEFAULT_CHECKBOX_STYLE = "\u2713" DEFAULT_RADIO_STYLE = "\u25CF" BUTTON_STYLES = { diff --git a/PyPDFForm/coordinate.py b/PyPDFForm/coordinate.py index e6492264..b698e95a 100644 --- a/PyPDFForm/coordinate.py +++ b/PyPDFForm/coordinate.py @@ -38,11 +38,11 @@ def get_draw_checkbox_radio_coordinates( ) -def get_draw_sig_coordinates_resolutions( +def get_draw_image_coordinates_resolutions( widget: dict, ) -> Tuple[float, float, float, float]: """ - Returns coordinates and resolutions to draw signature at given a PDF form signature widget. + Returns coordinates and resolutions to draw image at given a PDF form signature/image widget. """ x = float(widget[Rect][0]) diff --git a/PyPDFForm/filler.py b/PyPDFForm/filler.py index c9a53e9f..67638164 100644 --- a/PyPDFForm/filler.py +++ b/PyPDFForm/filler.py @@ -9,7 +9,7 @@ from .constants import WIDGET_TYPES, Annots from .coordinate import (get_draw_checkbox_radio_coordinates, - get_draw_sig_coordinates_resolutions, + get_draw_image_coordinates_resolutions, get_draw_text_coordinates, get_text_line_x_coordinates) from .font import checkbox_radio_font_size @@ -19,6 +19,7 @@ from .middleware.radio import Radio from .middleware.signature import Signature from .middleware.text import Text +from .middleware.image import Image from .patterns import (simple_flatten_generic, simple_flatten_radio, simple_update_checkbox_value, simple_update_dropdown_value, simple_update_radio_value, @@ -69,12 +70,12 @@ def fill( 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): + 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_sig_coordinates_resolutions(_widget) + x, y, width, height = get_draw_image_coordinates_resolutions(_widget) images_to_draw[page].append( [ stream, diff --git a/PyPDFForm/middleware/image.py b/PyPDFForm/middleware/image.py new file mode 100644 index 00000000..d2c27371 --- /dev/null +++ b/PyPDFForm/middleware/image.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +"""Contains image field middleware.""" + +from .signature import Signature + + +class Image(Signature): + """A class to represent an image field widget.""" diff --git a/PyPDFForm/patterns.py b/PyPDFForm/patterns.py index b85281b7..eef4f6a8 100644 --- a/PyPDFForm/patterns.py +++ b/PyPDFForm/patterns.py @@ -4,15 +4,21 @@ from pypdf.generic import (DictionaryObject, NameObject, NumberObject, TextStringObject) -from .constants import (AP, AS, CA, DA, FT, MK, READ_ONLY, Btn, Ch, D, Ff, Off, - Opt, Parent, Q, Sig, Subtype, T, Tx, V, Widget) +from .constants import (A, AP, AS, CA, DA, FT, MK, READ_ONLY, Btn, Ch, D, Ff, Off, + Opt, Parent, Q, Sig, Subtype, T, Tx, V, Widget, JS, + IMAGE_FIELD_IDENTIFIER) from .middleware.checkbox import Checkbox from .middleware.dropdown import Dropdown from .middleware.radio import Radio from .middleware.signature import Signature +from .middleware.image import Image from .middleware.text import Text WIDGET_TYPE_PATTERNS = [ + ( + ({A: {JS: IMAGE_FIELD_IDENTIFIER}},), + Image, + ), ( ({FT: Sig},), Signature, diff --git a/docs/fill.md b/docs/fill.md index 0761fe9f..36c46ad9 100644 --- a/docs/fill.md +++ b/docs/fill.md @@ -92,3 +92,27 @@ with open("output.pdf", "wb+") as output: **NOTE:** As described [here](install.md/#create-a-pdf-wrapper), the value of the signature in your dictionary can be a file path shown above, but also an open file object and a file stream that's in `bytes`. + +## Fill image widgets (beta) + +**NOTE:** This is a beta feature, meaning it still needs to be tested against more PDF forms and may not work for +some of them. + +An image field widget can be filled similarly to a signature field, by providing a value of file path, file object, or +file stream. + +Consider [this PDF](https://github.com/chinapandaman/PyPDFForm/raw/master/pdf_samples/sample_template_with_image_field.pdf) +and [this image](https://github.com/chinapandaman/PyPDFForm/raw/master/image_samples/sample_image.jpg): + +```python +from PyPDFForm import PdfWrapper + +filled = PdfWrapper("sample_template_with_image_field.pdf").fill( + { + "image_1": "sample_image.jpg" + }, +) + +with open("output.pdf", "wb+") as output: + output.write(filled.read()) +``` diff --git a/docs/simple_fill.md b/docs/simple_fill.md index 9d001d80..d2afe794 100644 --- a/docs/simple_fill.md +++ b/docs/simple_fill.md @@ -3,7 +3,7 @@ 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. +buttons, dropdowns, and paragraphs. However, it does NOT support signature or image widgets. Consider [this PDF](https://github.com/chinapandaman/PyPDFForm/raw/master/pdf_samples/dropdown/sample_template_with_dropdown.pdf): diff --git a/pdf_samples/sample_filled_image.pdf b/pdf_samples/sample_filled_image.pdf new file mode 100644 index 00000000..56a5dc60 Binary files /dev/null and b/pdf_samples/sample_filled_image.pdf differ diff --git a/pdf_samples/sample_template_with_image_field.pdf b/pdf_samples/sample_template_with_image_field.pdf new file mode 100644 index 00000000..9a51a5f4 Binary files /dev/null and b/pdf_samples/sample_template_with_image_field.pdf differ diff --git a/pdf_samples/scenario/issues/560.pdf b/pdf_samples/scenario/issues/560.pdf new file mode 100644 index 00000000..8de1483a Binary files /dev/null and b/pdf_samples/scenario/issues/560.pdf differ diff --git a/pdf_samples/scenario/issues/560_expected.pdf b/pdf_samples/scenario/issues/560_expected.pdf new file mode 100644 index 00000000..1ef015c6 Binary files /dev/null and b/pdf_samples/scenario/issues/560_expected.pdf differ diff --git a/tests/conftest.py b/tests/conftest.py index 650d8d8b..a6822ab6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -181,6 +181,12 @@ def sample_template_with_dropdown(pdf_samples): return f.read() +@pytest.fixture +def sample_template_with_image_field(pdf_samples): + with open(os.path.join(pdf_samples, "sample_template_with_image_field.pdf"), "rb+") as f: + return f.read() + + @pytest.fixture def dropdown_alignment(pdf_samples): with open( diff --git a/tests/scenario/test_issues.py b/tests/scenario/test_issues.py index 7916f085..483d08bf 100644 --- a/tests/scenario/test_issues.py +++ b/tests/scenario/test_issues.py @@ -122,3 +122,19 @@ def test_encrypted_edit_pdf_form(issue_pdf_directory, request): expected = f.read() assert len(obj.read()) == len(expected) assert obj.read() == expected + + +def test_fill_image(issue_pdf_directory, image_samples, request): + obj = PdfWrapper(os.path.join(issue_pdf_directory, "560.pdf")) + obj = obj.fill( + { + "ImageSign": os.path.join(image_samples, "sample_image.jpg") + } + ) + expected_path = os.path.join(issue_pdf_directory, "560_expected.pdf") + request.config.results["expected_path"] = expected_path + request.config.results["stream"] = obj.read() + with open(expected_path, "rb+") as f: + expected = f.read() + assert len(obj.read()) == len(expected) + assert obj.read() == expected diff --git a/tests/test_functional.py b/tests/test_functional.py index 9bbe7b2d..f433d427 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -610,3 +610,21 @@ def test_radio_change_size_and_button_style( assert len(obj.read()) == len(expected) assert obj.stream == expected + + +def test_fill_image(sample_template_with_image_field, image_samples, pdf_samples, request): + expected_path = os.path.join(pdf_samples, "sample_filled_image.pdf") + with open(expected_path, "rb+") as f: + obj = PdfWrapper(sample_template_with_image_field).fill( + { + "image_1": os.path.join(image_samples, "sample_image.jpg") + }, + ) + + 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