PyPDFForm 1.5.7__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of PyPDFForm might be problematic. Click here for more details.
- PyPDFForm/__init__.py +1 -1
- PyPDFForm/constants.py +8 -0
- PyPDFForm/coordinate.py +54 -9
- PyPDFForm/filler.py +75 -10
- PyPDFForm/font.py +30 -33
- PyPDFForm/middleware/base.py +6 -0
- PyPDFForm/middleware/checkbox.py +1 -1
- PyPDFForm/middleware/radio.py +12 -1
- PyPDFForm/patterns.py +18 -3
- PyPDFForm/template.py +57 -98
- PyPDFForm/utils.py +45 -5
- PyPDFForm/watermark.py +54 -11
- PyPDFForm/widgets/base.py +5 -4
- PyPDFForm/wrapper.py +29 -21
- {pypdfform-1.5.7.dist-info → pypdfform-2.0.0.dist-info}/METADATA +12 -2
- pypdfform-2.0.0.dist-info/RECORD +30 -0
- {pypdfform-1.5.7.dist-info → pypdfform-2.0.0.dist-info}/WHEEL +1 -1
- pypdfform-1.5.7.dist-info/RECORD +0 -30
- {pypdfform-1.5.7.dist-info → pypdfform-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {pypdfform-1.5.7.dist-info → pypdfform-2.0.0.dist-info}/top_level.txt +0 -0
PyPDFForm/__init__.py
CHANGED
PyPDFForm/constants.py
CHANGED
|
@@ -50,6 +50,13 @@ Ch = "/Ch"
|
|
|
50
50
|
Opt = "/Opt"
|
|
51
51
|
MK = "/MK"
|
|
52
52
|
CA = "/CA"
|
|
53
|
+
BC = "/BC"
|
|
54
|
+
BG = "/BG"
|
|
55
|
+
BS = "/BS"
|
|
56
|
+
W = "/W"
|
|
57
|
+
S = "/S"
|
|
58
|
+
D = "/D"
|
|
59
|
+
U = "/U"
|
|
53
60
|
AS = "/AS"
|
|
54
61
|
Yes = "/Yes"
|
|
55
62
|
Off = "/Off"
|
|
@@ -66,6 +73,7 @@ COMB = 1 << 24
|
|
|
66
73
|
|
|
67
74
|
FONT_SIZE_IDENTIFIER = "Tf"
|
|
68
75
|
FONT_COLOR_IDENTIFIER = " rg"
|
|
76
|
+
DEFAULT_BORDER_WIDTH = 1
|
|
69
77
|
DEFAULT_FONT = "Helvetica"
|
|
70
78
|
DEFAULT_FONT_SIZE = 12
|
|
71
79
|
DEFAULT_FONT_COLOR = (0, 0, 0)
|
PyPDFForm/coordinate.py
CHANGED
|
@@ -10,21 +10,58 @@ from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
|
10
10
|
from .constants import (COORDINATE_GRID_FONT_SIZE_MARGIN_RATIO, DEFAULT_FONT,
|
|
11
11
|
Rect)
|
|
12
12
|
from .middleware.text import Text
|
|
13
|
-
from .
|
|
14
|
-
|
|
15
|
-
from .utils import stream_to_io
|
|
13
|
+
from .patterns import WIDGET_ALIGNMENT_PATTERNS
|
|
14
|
+
from .template import get_char_rect_width, is_text_multiline
|
|
15
|
+
from .utils import extract_widget_property, handle_color, stream_to_io
|
|
16
16
|
from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def get_draw_border_coordinates(widget: dict, shape: str) -> List[float]:
|
|
20
|
+
"""Returns coordinates to draw each widget's border."""
|
|
21
|
+
|
|
22
|
+
result = [
|
|
23
|
+
float(widget[Rect][0]),
|
|
24
|
+
float(widget[Rect][1]),
|
|
25
|
+
abs(float(widget[Rect][0]) - float(widget[Rect][2])),
|
|
26
|
+
abs(float(widget[Rect][1]) - float(widget[Rect][3])),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
if shape == "ellipse":
|
|
30
|
+
width = abs(float(widget[Rect][0]) - float(widget[Rect][2]))
|
|
31
|
+
height = abs(float(widget[Rect][1]) - float(widget[Rect][3]))
|
|
32
|
+
|
|
33
|
+
width_mid = (float(widget[Rect][0]) + float(widget[Rect][2])) / 2
|
|
34
|
+
height_mid = (float(widget[Rect][1]) + float(widget[Rect][3])) / 2
|
|
35
|
+
|
|
36
|
+
less = min(width, height)
|
|
37
|
+
|
|
38
|
+
result = [
|
|
39
|
+
width_mid - less / 2,
|
|
40
|
+
height_mid - less / 2,
|
|
41
|
+
width_mid + less / 2,
|
|
42
|
+
height_mid + less / 2,
|
|
43
|
+
]
|
|
44
|
+
elif shape == "line":
|
|
45
|
+
result = [
|
|
46
|
+
float(widget[Rect][0]),
|
|
47
|
+
float(widget[Rect][1]),
|
|
48
|
+
float(widget[Rect][2]),
|
|
49
|
+
float(widget[Rect][1]),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
19
55
|
def get_draw_checkbox_radio_coordinates(
|
|
20
56
|
widget: dict,
|
|
21
57
|
widget_middleware: Text,
|
|
58
|
+
border_width: int,
|
|
22
59
|
) -> Tuple[Union[float, int], Union[float, int]]:
|
|
23
60
|
"""Returns coordinates to draw at given a PDF form checkbox/radio widget."""
|
|
24
61
|
|
|
25
|
-
string_height = widget_middleware.font_size *
|
|
62
|
+
string_height = widget_middleware.font_size * 72 / 96
|
|
26
63
|
width_mid_point = (float(widget[Rect][0]) + float(widget[Rect][2])) / 2
|
|
27
|
-
|
|
64
|
+
half_widget_height = abs(float(widget[Rect][1]) - float(widget[Rect][3])) / 2
|
|
28
65
|
|
|
29
66
|
return (
|
|
30
67
|
width_mid_point
|
|
@@ -34,7 +71,9 @@ def get_draw_checkbox_radio_coordinates(
|
|
|
34
71
|
widget_middleware.font_size,
|
|
35
72
|
)
|
|
36
73
|
/ 2,
|
|
37
|
-
(
|
|
74
|
+
float(widget[Rect][1])
|
|
75
|
+
+ (half_widget_height - string_height / 2)
|
|
76
|
+
+ border_width / 2,
|
|
38
77
|
)
|
|
39
78
|
|
|
40
79
|
|
|
@@ -96,7 +135,9 @@ def get_draw_text_coordinates(
|
|
|
96
135
|
else widget_middleware.character_paddings
|
|
97
136
|
)
|
|
98
137
|
|
|
99
|
-
alignment =
|
|
138
|
+
alignment = (
|
|
139
|
+
extract_widget_property(widget, WIDGET_ALIGNMENT_PATTERNS, None, int) or 0
|
|
140
|
+
)
|
|
100
141
|
x = float(widget[Rect][0])
|
|
101
142
|
|
|
102
143
|
if int(alignment) != 0:
|
|
@@ -196,12 +237,16 @@ def generate_coordinate_grid(
|
|
|
196
237
|
|
|
197
238
|
current = margin
|
|
198
239
|
while current < width:
|
|
199
|
-
lines_by_page[i + 1].append(
|
|
240
|
+
lines_by_page[i + 1].append(
|
|
241
|
+
[current, 0, current, height, handle_color([r, g, b]), None, 1, None]
|
|
242
|
+
)
|
|
200
243
|
current += margin
|
|
201
244
|
|
|
202
245
|
current = margin
|
|
203
246
|
while current < height:
|
|
204
|
-
lines_by_page[i + 1].append(
|
|
247
|
+
lines_by_page[i + 1].append(
|
|
248
|
+
[0, current, width, current, handle_color([r, g, b]), None, 1, None]
|
|
249
|
+
)
|
|
205
250
|
current += margin
|
|
206
251
|
|
|
207
252
|
x = margin
|
PyPDFForm/filler.py
CHANGED
|
@@ -7,8 +7,10 @@ from typing import Dict, Tuple, Union, cast
|
|
|
7
7
|
from pypdf import PdfReader, PdfWriter
|
|
8
8
|
from pypdf.generic import BooleanObject, DictionaryObject, NameObject
|
|
9
9
|
|
|
10
|
-
from .constants import
|
|
11
|
-
|
|
10
|
+
from .constants import (BUTTON_STYLES, DEFAULT_RADIO_STYLE, WIDGET_TYPES,
|
|
11
|
+
AcroForm, Annots, NeedAppearances, Root, U)
|
|
12
|
+
from .coordinate import (get_draw_border_coordinates,
|
|
13
|
+
get_draw_checkbox_radio_coordinates,
|
|
12
14
|
get_draw_image_coordinates_resolutions,
|
|
13
15
|
get_draw_text_coordinates,
|
|
14
16
|
get_text_line_x_coordinates)
|
|
@@ -20,12 +22,13 @@ from .middleware.image import Image
|
|
|
20
22
|
from .middleware.radio import Radio
|
|
21
23
|
from .middleware.signature import Signature
|
|
22
24
|
from .middleware.text import Text
|
|
23
|
-
from .patterns import (
|
|
24
|
-
simple_update_checkbox_value,
|
|
25
|
+
from .patterns import (WIDGET_KEY_PATTERNS, simple_flatten_generic,
|
|
26
|
+
simple_flatten_radio, simple_update_checkbox_value,
|
|
25
27
|
simple_update_dropdown_value, simple_update_radio_value,
|
|
26
28
|
simple_update_text_value)
|
|
27
|
-
from .template import
|
|
28
|
-
from .utils import checkbox_radio_to_draw,
|
|
29
|
+
from .template import get_widgets_by_page
|
|
30
|
+
from .utils import (checkbox_radio_to_draw, extract_widget_property,
|
|
31
|
+
stream_to_io)
|
|
29
32
|
from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
|
|
30
33
|
|
|
31
34
|
|
|
@@ -38,7 +41,9 @@ def check_radio_handler(
|
|
|
38
41
|
checkbox_radio_font_size(widget) if middleware.size is None else middleware.size
|
|
39
42
|
)
|
|
40
43
|
to_draw = checkbox_radio_to_draw(middleware, font_size)
|
|
41
|
-
x, y = get_draw_checkbox_radio_coordinates(
|
|
44
|
+
x, y = get_draw_checkbox_radio_coordinates(
|
|
45
|
+
widget, to_draw, border_width=middleware.border_width
|
|
46
|
+
)
|
|
42
47
|
text_needs_to_be_drawn = False
|
|
43
48
|
if type(middleware) is Checkbox and middleware.value:
|
|
44
49
|
text_needs_to_be_drawn = True
|
|
@@ -91,6 +96,45 @@ def text_handler(
|
|
|
91
96
|
return to_draw, x, y, text_needs_to_be_drawn
|
|
92
97
|
|
|
93
98
|
|
|
99
|
+
def border_handler(
|
|
100
|
+
widget: dict,
|
|
101
|
+
middleware: WIDGET_TYPES,
|
|
102
|
+
rect_borders_to_draw: list,
|
|
103
|
+
ellipse_borders_to_draw: list,
|
|
104
|
+
line_borders_to_draw: list,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Handles draw parameters for each widget's border."""
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
isinstance(middleware, Radio)
|
|
110
|
+
and BUTTON_STYLES.get(middleware.button_style) == DEFAULT_RADIO_STYLE
|
|
111
|
+
):
|
|
112
|
+
list_to_append = ellipse_borders_to_draw
|
|
113
|
+
shape = "ellipse"
|
|
114
|
+
elif middleware.border_style == U:
|
|
115
|
+
list_to_append = line_borders_to_draw
|
|
116
|
+
shape = "line"
|
|
117
|
+
else:
|
|
118
|
+
list_to_append = rect_borders_to_draw
|
|
119
|
+
shape = "rect"
|
|
120
|
+
|
|
121
|
+
list_to_append.append(
|
|
122
|
+
get_draw_border_coordinates(widget, shape)
|
|
123
|
+
+ [
|
|
124
|
+
middleware.border_color,
|
|
125
|
+
middleware.background_color,
|
|
126
|
+
middleware.border_width,
|
|
127
|
+
middleware.dash_array,
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if shape == "line":
|
|
132
|
+
rect_borders_to_draw.append(
|
|
133
|
+
get_draw_border_coordinates(widget, "rect")
|
|
134
|
+
+ [None, middleware.background_color, 0, None]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
94
138
|
def get_drawn_stream(to_draw: dict, stream: bytes, action: str) -> bytes:
|
|
95
139
|
"""Generates a stream of an input PDF stream with stuff drawn on it."""
|
|
96
140
|
|
|
@@ -113,6 +157,9 @@ def fill(
|
|
|
113
157
|
|
|
114
158
|
texts_to_draw = {}
|
|
115
159
|
images_to_draw = {}
|
|
160
|
+
rect_borders_to_draw = {}
|
|
161
|
+
ellipse_borders_to_draw = {}
|
|
162
|
+
line_borders_to_draw = {}
|
|
116
163
|
any_image_to_draw = False
|
|
117
164
|
|
|
118
165
|
radio_button_tracker = {}
|
|
@@ -120,11 +167,23 @@ def fill(
|
|
|
120
167
|
for page, widget_dicts in get_widgets_by_page(template_stream).items():
|
|
121
168
|
texts_to_draw[page] = []
|
|
122
169
|
images_to_draw[page] = []
|
|
170
|
+
rect_borders_to_draw[page] = []
|
|
171
|
+
ellipse_borders_to_draw[page] = []
|
|
172
|
+
line_borders_to_draw[page] = []
|
|
123
173
|
for widget_dict in widget_dicts:
|
|
124
|
-
key =
|
|
174
|
+
key = extract_widget_property(widget_dict, WIDGET_KEY_PATTERNS, None, str)
|
|
125
175
|
text_needs_to_be_drawn = False
|
|
126
176
|
to_draw = x = y = None
|
|
127
177
|
|
|
178
|
+
if widgets[key].render_widget:
|
|
179
|
+
border_handler(
|
|
180
|
+
widget_dict,
|
|
181
|
+
widgets[key],
|
|
182
|
+
rect_borders_to_draw[page],
|
|
183
|
+
ellipse_borders_to_draw[page],
|
|
184
|
+
line_borders_to_draw[page],
|
|
185
|
+
)
|
|
186
|
+
|
|
128
187
|
if isinstance(widgets[key], (Checkbox, Radio)):
|
|
129
188
|
to_draw, x, y, text_needs_to_be_drawn = check_radio_handler(
|
|
130
189
|
widget_dict, widgets[key], radio_button_tracker
|
|
@@ -154,7 +213,11 @@ def fill(
|
|
|
154
213
|
]
|
|
155
214
|
)
|
|
156
215
|
|
|
157
|
-
result =
|
|
216
|
+
result = template_stream
|
|
217
|
+
result = get_drawn_stream(rect_borders_to_draw, result, "rect")
|
|
218
|
+
result = get_drawn_stream(ellipse_borders_to_draw, result, "ellipse")
|
|
219
|
+
result = get_drawn_stream(line_borders_to_draw, result, "line")
|
|
220
|
+
result = get_drawn_stream(texts_to_draw, result, "text")
|
|
158
221
|
|
|
159
222
|
if any_image_to_draw:
|
|
160
223
|
result = get_drawn_stream(images_to_draw, result, "image")
|
|
@@ -189,7 +252,9 @@ def simple_fill(
|
|
|
189
252
|
for page in out.pages:
|
|
190
253
|
for annot in page.get(Annots, []): # noqa
|
|
191
254
|
annot = cast(DictionaryObject, annot.get_object())
|
|
192
|
-
key =
|
|
255
|
+
key = extract_widget_property(
|
|
256
|
+
annot.get_object(), WIDGET_KEY_PATTERNS, None, str
|
|
257
|
+
)
|
|
193
258
|
|
|
194
259
|
widget = widgets.get(key)
|
|
195
260
|
if widget is None or widget.value is None:
|
PyPDFForm/font.py
CHANGED
|
@@ -16,7 +16,7 @@ from .constants import (DEFAULT_FONT, FONT_COLOR_IDENTIFIER,
|
|
|
16
16
|
MARGIN_BETWEEN_LINES, Rect)
|
|
17
17
|
from .middleware.text import Text
|
|
18
18
|
from .patterns import TEXT_FIELD_APPEARANCE_PATTERNS
|
|
19
|
-
from .utils import
|
|
19
|
+
from .utils import extract_widget_property
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def register_font(font_name: str, ttf_stream: bytes) -> bool:
|
|
@@ -72,19 +72,14 @@ def extract_font_from_text_appearance(text_appearance: str) -> Union[str, None]:
|
|
|
72
72
|
def auto_detect_font(widget: dict) -> str:
|
|
73
73
|
"""Returns the font of the text field if it is one of the standard fonts."""
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
for pattern in TEXT_FIELD_APPEARANCE_PATTERNS:
|
|
79
|
-
text_appearance = traverse_pattern(pattern, widget)
|
|
80
|
-
|
|
81
|
-
if text_appearance:
|
|
82
|
-
break
|
|
75
|
+
text_appearance = extract_widget_property(
|
|
76
|
+
widget, TEXT_FIELD_APPEARANCE_PATTERNS, None, None
|
|
77
|
+
)
|
|
83
78
|
|
|
84
79
|
if not text_appearance:
|
|
85
|
-
return
|
|
80
|
+
return DEFAULT_FONT
|
|
86
81
|
|
|
87
|
-
return extract_font_from_text_appearance(text_appearance) or
|
|
82
|
+
return extract_font_from_text_appearance(text_appearance) or DEFAULT_FONT
|
|
88
83
|
|
|
89
84
|
|
|
90
85
|
def text_field_font_size(widget: dict) -> Union[float, int]:
|
|
@@ -115,13 +110,14 @@ def get_text_field_font_size(widget: dict) -> Union[float, int]:
|
|
|
115
110
|
"""Returns the font size of the text field if presented or zero."""
|
|
116
111
|
|
|
117
112
|
result = 0
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
text_appearance = extract_widget_property(
|
|
114
|
+
widget, TEXT_FIELD_APPEARANCE_PATTERNS, None, None
|
|
115
|
+
)
|
|
116
|
+
if text_appearance:
|
|
117
|
+
properties = text_appearance.split(" ")
|
|
118
|
+
for i, val in enumerate(properties):
|
|
119
|
+
if val.startswith(FONT_SIZE_IDENTIFIER):
|
|
120
|
+
return float(properties[i - 1])
|
|
125
121
|
|
|
126
122
|
return result
|
|
127
123
|
|
|
@@ -132,21 +128,22 @@ def get_text_field_font_color(
|
|
|
132
128
|
"""Returns the font color tuple of the text field if presented or black."""
|
|
133
129
|
|
|
134
130
|
result = (0, 0, 0)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
|
|
131
|
+
text_appearance = extract_widget_property(
|
|
132
|
+
widget, TEXT_FIELD_APPEARANCE_PATTERNS, None, None
|
|
133
|
+
)
|
|
134
|
+
if text_appearance:
|
|
135
|
+
if FONT_COLOR_IDENTIFIER not in text_appearance:
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
text_appearance = text_appearance.split(" ")
|
|
139
|
+
for i, val in enumerate(text_appearance):
|
|
140
|
+
if val.startswith(FONT_COLOR_IDENTIFIER.replace(" ", "")):
|
|
141
|
+
result = (
|
|
142
|
+
float(text_appearance[i - 3]),
|
|
143
|
+
float(text_appearance[i - 2]),
|
|
144
|
+
float(text_appearance[i - 1]),
|
|
145
|
+
)
|
|
146
|
+
break
|
|
150
147
|
|
|
151
148
|
return result
|
|
152
149
|
|
PyPDFForm/middleware/base.py
CHANGED
|
@@ -19,6 +19,12 @@ class Widget:
|
|
|
19
19
|
self.full_name = None
|
|
20
20
|
self._value = value
|
|
21
21
|
self.desc = None
|
|
22
|
+
self.border_color = None
|
|
23
|
+
self.background_color = None
|
|
24
|
+
self.border_width = None
|
|
25
|
+
self.border_style = None
|
|
26
|
+
self.dash_array = None
|
|
27
|
+
self.render_widget = None
|
|
22
28
|
|
|
23
29
|
@property
|
|
24
30
|
def name(self) -> str:
|
PyPDFForm/middleware/checkbox.py
CHANGED
PyPDFForm/middleware/radio.py
CHANGED
|
@@ -7,7 +7,18 @@ from .checkbox import Checkbox
|
|
|
7
7
|
class Radio(Checkbox):
|
|
8
8
|
"""A class to represent a radiobutton widget."""
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
name: str,
|
|
13
|
+
value: int = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Constructs all attributes for the radiobutton."""
|
|
16
|
+
|
|
17
|
+
super().__init__(name, value)
|
|
18
|
+
|
|
19
|
+
self.size = None
|
|
20
|
+
self.number_of_options = 0
|
|
21
|
+
self._button_style = self.BUTTON_STYLE_MAPPING["circle"]
|
|
11
22
|
|
|
12
23
|
@property
|
|
13
24
|
def schema_definition(self) -> dict:
|
PyPDFForm/patterns.py
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
from pypdf.generic import (DictionaryObject, NameObject, NumberObject,
|
|
5
5
|
TextStringObject)
|
|
6
6
|
|
|
7
|
-
from .constants import (AP, AS,
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
from .constants import (AP, AS, BC, BG, BS, CA, DA, DV, FT,
|
|
8
|
+
IMAGE_FIELD_IDENTIFIER, JS, MK, MULTILINE, READ_ONLY,
|
|
9
|
+
TU, A, Btn, Ch, D, Ff, N, Off, Opt, Parent, Q, S, Sig,
|
|
10
|
+
T, Tx, V, W, Yes)
|
|
10
11
|
from .middleware.checkbox import Checkbox
|
|
11
12
|
from .middleware.dropdown import Dropdown
|
|
12
13
|
from .middleware.image import Image
|
|
@@ -94,6 +95,20 @@ BUTTON_STYLE_PATTERNS = [
|
|
|
94
95
|
{MK: {CA: True}},
|
|
95
96
|
]
|
|
96
97
|
|
|
98
|
+
BORDER_COLOR_PATTERNS = [
|
|
99
|
+
{MK: {BC: True}},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
BACKGROUND_COLOR_PATTERNS = [
|
|
103
|
+
{MK: {BG: True}},
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
BORDER_WIDTH_PATTERNS = [{BS: {W: True}}]
|
|
107
|
+
|
|
108
|
+
BORDER_STYLE_PATTERNS = [{BS: {S: True}}]
|
|
109
|
+
|
|
110
|
+
BORDER_DASH_ARRAY_PATTERNS = [{BS: {D: True}}]
|
|
111
|
+
|
|
97
112
|
|
|
98
113
|
def simple_update_checkbox_value(annot: DictionaryObject, check: bool = False) -> None:
|
|
99
114
|
"""Patterns to update values for checkbox annotations."""
|
PyPDFForm/template.py
CHANGED
|
@@ -10,8 +10,9 @@ from pypdf import PdfReader, PdfWriter
|
|
|
10
10
|
from pypdf.generic import DictionaryObject
|
|
11
11
|
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
12
12
|
|
|
13
|
-
from .constants import (COMB,
|
|
14
|
-
|
|
13
|
+
from .constants import (COMB, DEFAULT_BORDER_WIDTH, DEFAULT_FONT_SIZE,
|
|
14
|
+
MULTILINE, NEW_LINE_SYMBOL, WIDGET_TYPES, Annots,
|
|
15
|
+
MaxLen, Parent, Rect, T)
|
|
15
16
|
from .font import (adjust_paragraph_font_size, adjust_text_field_font_size,
|
|
16
17
|
auto_detect_font, get_text_field_font_color,
|
|
17
18
|
get_text_field_font_size, text_field_font_size)
|
|
@@ -19,12 +20,14 @@ from .middleware.checkbox import Checkbox
|
|
|
19
20
|
from .middleware.dropdown import Dropdown
|
|
20
21
|
from .middleware.radio import Radio
|
|
21
22
|
from .middleware.text import Text
|
|
22
|
-
from .patterns import (
|
|
23
|
-
|
|
23
|
+
from .patterns import (BACKGROUND_COLOR_PATTERNS, BORDER_COLOR_PATTERNS,
|
|
24
|
+
BORDER_DASH_ARRAY_PATTERNS, BORDER_STYLE_PATTERNS,
|
|
25
|
+
BORDER_WIDTH_PATTERNS, BUTTON_STYLE_PATTERNS,
|
|
26
|
+
DROPDOWN_CHOICE_PATTERNS, TEXT_FIELD_FLAG_PATTERNS,
|
|
24
27
|
WIDGET_DESCRIPTION_PATTERNS, WIDGET_KEY_PATTERNS,
|
|
25
28
|
WIDGET_TYPE_PATTERNS, update_annotation_name)
|
|
26
|
-
from .utils import find_pattern_match,
|
|
27
|
-
|
|
29
|
+
from .utils import (extract_widget_property, find_pattern_match, handle_color,
|
|
30
|
+
stream_to_io)
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
def set_character_x_paddings(
|
|
@@ -34,7 +37,7 @@ def set_character_x_paddings(
|
|
|
34
37
|
|
|
35
38
|
for _widgets in get_widgets_by_page(pdf_stream).values():
|
|
36
39
|
for widget in _widgets:
|
|
37
|
-
key =
|
|
40
|
+
key = extract_widget_property(widget, WIDGET_KEY_PATTERNS, None, str)
|
|
38
41
|
_widget = widgets[key]
|
|
39
42
|
|
|
40
43
|
if isinstance(_widget, Text) and _widget.comb is True:
|
|
@@ -44,7 +47,9 @@ def set_character_x_paddings(
|
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
def build_widgets(
|
|
47
|
-
pdf_stream: bytes,
|
|
50
|
+
pdf_stream: bytes,
|
|
51
|
+
use_full_widget_name: bool,
|
|
52
|
+
render_widgets: bool,
|
|
48
53
|
) -> Dict[str, WIDGET_TYPES]:
|
|
49
54
|
"""Builds a widget dict given a PDF form stream."""
|
|
50
55
|
|
|
@@ -52,19 +57,44 @@ def build_widgets(
|
|
|
52
57
|
|
|
53
58
|
for widgets in get_widgets_by_page(pdf_stream).values():
|
|
54
59
|
for widget in widgets:
|
|
55
|
-
key =
|
|
60
|
+
key = extract_widget_property(widget, WIDGET_KEY_PATTERNS, None, str)
|
|
56
61
|
_widget = construct_widget(widget, key)
|
|
57
62
|
if _widget is not None:
|
|
58
63
|
if use_full_widget_name:
|
|
59
64
|
_widget.full_name = get_widget_full_key(widget)
|
|
60
|
-
|
|
65
|
+
|
|
66
|
+
_widget.render_widget = render_widgets
|
|
67
|
+
_widget.desc = extract_widget_property(
|
|
68
|
+
widget, WIDGET_DESCRIPTION_PATTERNS, None, str
|
|
69
|
+
)
|
|
70
|
+
_widget.border_color = extract_widget_property(
|
|
71
|
+
widget, BORDER_COLOR_PATTERNS, None, handle_color
|
|
72
|
+
)
|
|
73
|
+
_widget.background_color = extract_widget_property(
|
|
74
|
+
widget, BACKGROUND_COLOR_PATTERNS, None, handle_color
|
|
75
|
+
)
|
|
76
|
+
_widget.border_width = extract_widget_property(
|
|
77
|
+
widget, BORDER_WIDTH_PATTERNS, DEFAULT_BORDER_WIDTH, float
|
|
78
|
+
)
|
|
79
|
+
_widget.border_style = extract_widget_property(
|
|
80
|
+
widget, BORDER_STYLE_PATTERNS, None, str
|
|
81
|
+
)
|
|
82
|
+
_widget.dash_array = extract_widget_property(
|
|
83
|
+
widget, BORDER_DASH_ARRAY_PATTERNS, None, list
|
|
84
|
+
)
|
|
85
|
+
|
|
61
86
|
if isinstance(_widget, Text):
|
|
62
87
|
_widget.max_length = get_text_field_max_length(widget)
|
|
63
88
|
if _widget.max_length is not None and is_text_field_comb(widget):
|
|
64
89
|
_widget.comb = True
|
|
65
90
|
|
|
66
91
|
if isinstance(_widget, (Checkbox, Radio)):
|
|
67
|
-
_widget.button_style =
|
|
92
|
+
_widget.button_style = (
|
|
93
|
+
extract_widget_property(
|
|
94
|
+
widget, BUTTON_STYLE_PATTERNS, None, str
|
|
95
|
+
)
|
|
96
|
+
or _widget.button_style
|
|
97
|
+
)
|
|
68
98
|
|
|
69
99
|
if isinstance(_widget, Dropdown):
|
|
70
100
|
_widget.choices = get_dropdown_choices(widget)
|
|
@@ -82,32 +112,14 @@ def build_widgets(
|
|
|
82
112
|
return results
|
|
83
113
|
|
|
84
114
|
|
|
85
|
-
def widget_rect_watermarks(pdf: bytes) -> List[bytes]:
|
|
86
|
-
"""Draws the rectangular border of each widget and returns watermarks."""
|
|
87
|
-
|
|
88
|
-
watermarks = []
|
|
89
|
-
|
|
90
|
-
for page, widgets in get_widgets_by_page(pdf).items():
|
|
91
|
-
to_draw = []
|
|
92
|
-
for widget in widgets:
|
|
93
|
-
rect = widget[Rect]
|
|
94
|
-
x = rect[0]
|
|
95
|
-
y = rect[1]
|
|
96
|
-
width = abs(rect[0] - rect[2])
|
|
97
|
-
height = abs(rect[1] - rect[3])
|
|
98
|
-
|
|
99
|
-
to_draw.append([x, y, width, height])
|
|
100
|
-
watermarks.append(
|
|
101
|
-
create_watermarks_and_draw(pdf, page, "rect", to_draw)[page - 1]
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
return watermarks
|
|
105
|
-
|
|
106
|
-
|
|
107
115
|
def dropdown_to_text(dropdown: Dropdown) -> Text:
|
|
108
116
|
"""Converts a dropdown widget to a text widget."""
|
|
109
117
|
|
|
110
118
|
result = Text(dropdown.name)
|
|
119
|
+
result.border_color = dropdown.border_color
|
|
120
|
+
result.background_color = dropdown.background_color
|
|
121
|
+
result.border_width = dropdown.border_width
|
|
122
|
+
result.render_widget = dropdown.render_widget
|
|
111
123
|
|
|
112
124
|
if dropdown.value is not None:
|
|
113
125
|
result.value = (
|
|
@@ -127,7 +139,7 @@ def update_text_field_attributes(
|
|
|
127
139
|
|
|
128
140
|
for _widgets in get_widgets_by_page(template_stream).values():
|
|
129
141
|
for _widget in _widgets:
|
|
130
|
-
key =
|
|
142
|
+
key = extract_widget_property(_widget, WIDGET_KEY_PATTERNS, None, str)
|
|
131
143
|
|
|
132
144
|
if isinstance(widgets[key], Text):
|
|
133
145
|
should_adjust_font_size = False
|
|
@@ -184,25 +196,13 @@ def get_widgets_by_page(pdf: bytes) -> Dict[int, List[dict]]:
|
|
|
184
196
|
return result
|
|
185
197
|
|
|
186
198
|
|
|
187
|
-
def get_widget_key(widget: dict) -> Union[str, list, None]:
|
|
188
|
-
"""Finds a PDF widget's annotated key by pattern matching."""
|
|
189
|
-
|
|
190
|
-
result = None
|
|
191
|
-
for pattern in WIDGET_KEY_PATTERNS:
|
|
192
|
-
value = traverse_pattern(pattern, widget)
|
|
193
|
-
if value:
|
|
194
|
-
result = value
|
|
195
|
-
break
|
|
196
|
-
return result
|
|
197
|
-
|
|
198
|
-
|
|
199
199
|
def get_widget_full_key(widget: dict) -> Union[str, None]:
|
|
200
200
|
"""
|
|
201
201
|
Returns a PDF widget's full annotated key by prepending its
|
|
202
202
|
parent widget's key.
|
|
203
203
|
"""
|
|
204
204
|
|
|
205
|
-
key =
|
|
205
|
+
key = extract_widget_property(widget, WIDGET_KEY_PATTERNS, None, str)
|
|
206
206
|
|
|
207
207
|
if (
|
|
208
208
|
Parent in widget
|
|
@@ -214,18 +214,6 @@ def get_widget_full_key(widget: dict) -> Union[str, None]:
|
|
|
214
214
|
return None
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
def get_widget_alignment(widget: dict) -> Union[str, list, None]:
|
|
218
|
-
"""Finds a PDF widget's alignment by pattern matching."""
|
|
219
|
-
|
|
220
|
-
result = None
|
|
221
|
-
for pattern in WIDGET_ALIGNMENT_PATTERNS:
|
|
222
|
-
value = traverse_pattern(pattern, widget)
|
|
223
|
-
if value:
|
|
224
|
-
result = value
|
|
225
|
-
break
|
|
226
|
-
return result
|
|
227
|
-
|
|
228
|
-
|
|
229
217
|
def construct_widget(widget: dict, key: str) -> Union[WIDGET_TYPES, None]:
|
|
230
218
|
"""Finds a PDF widget's annotated type by pattern matching."""
|
|
231
219
|
|
|
@@ -247,26 +235,10 @@ def get_text_field_max_length(widget: dict) -> Union[int, None]:
|
|
|
247
235
|
return int(widget[MaxLen]) or None if MaxLen in widget else None
|
|
248
236
|
|
|
249
237
|
|
|
250
|
-
def get_widget_description(widget: dict) -> Union[str, None]:
|
|
251
|
-
"""Returns the description of the widget if presented or None."""
|
|
252
|
-
|
|
253
|
-
result = None
|
|
254
|
-
for pattern in WIDGET_DESCRIPTION_PATTERNS:
|
|
255
|
-
value = traverse_pattern(pattern, widget)
|
|
256
|
-
if value:
|
|
257
|
-
result = str(value)
|
|
258
|
-
break
|
|
259
|
-
return result
|
|
260
|
-
|
|
261
|
-
|
|
262
238
|
def check_field_flag_bit(widget: dict, bit: int) -> bool:
|
|
263
239
|
"""Checks if a bit is set in a widget's field flag."""
|
|
264
240
|
|
|
265
|
-
field_flag = None
|
|
266
|
-
for pattern in TEXT_FIELD_FLAG_PATTERNS:
|
|
267
|
-
field_flag = traverse_pattern(pattern, widget)
|
|
268
|
-
if field_flag is not None:
|
|
269
|
-
break
|
|
241
|
+
field_flag = extract_widget_property(widget, TEXT_FIELD_FLAG_PATTERNS, None, int)
|
|
270
242
|
|
|
271
243
|
if field_flag is None:
|
|
272
244
|
return False
|
|
@@ -289,27 +261,12 @@ def is_text_multiline(widget: dict) -> bool:
|
|
|
289
261
|
def get_dropdown_choices(widget: dict) -> Union[Tuple[str, ...], None]:
|
|
290
262
|
"""Returns string options of a dropdown field."""
|
|
291
263
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
)
|
|
299
|
-
break
|
|
300
|
-
|
|
301
|
-
return result
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def get_button_style(widget: dict) -> Union[str, None]:
|
|
305
|
-
"""Returns the button style of a checkbox or radiobutton."""
|
|
306
|
-
|
|
307
|
-
for pattern in BUTTON_STYLE_PATTERNS:
|
|
308
|
-
style = traverse_pattern(pattern, widget)
|
|
309
|
-
if style is not None:
|
|
310
|
-
return str(style)
|
|
311
|
-
|
|
312
|
-
return None
|
|
264
|
+
return tuple(
|
|
265
|
+
(each if isinstance(each, str) else str(each[1]))
|
|
266
|
+
for each in extract_widget_property(
|
|
267
|
+
widget, DROPDOWN_CHOICE_PATTERNS, None, None
|
|
268
|
+
)
|
|
269
|
+
)
|
|
313
270
|
|
|
314
271
|
|
|
315
272
|
def get_char_rect_width(widget: dict, widget_middleware: Text) -> float:
|
|
@@ -463,7 +420,9 @@ def update_widget_keys(
|
|
|
463
420
|
for page in out.pages:
|
|
464
421
|
for annot in page.get(Annots, []): # noqa
|
|
465
422
|
annot = cast(DictionaryObject, annot.get_object())
|
|
466
|
-
key =
|
|
423
|
+
key = extract_widget_property(
|
|
424
|
+
annot.get_object(), WIDGET_KEY_PATTERNS, None, str
|
|
425
|
+
)
|
|
467
426
|
|
|
468
427
|
widget = widgets.get(key)
|
|
469
428
|
if widget is None:
|
PyPDFForm/utils.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""Contains utility helpers."""
|
|
3
3
|
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from io import BytesIO
|
|
5
6
|
from secrets import choice
|
|
6
7
|
from string import ascii_letters, digits, punctuation
|
|
7
|
-
from typing import BinaryIO, List, Union
|
|
8
|
+
from typing import Any, BinaryIO, List, Union
|
|
8
9
|
|
|
9
10
|
from pypdf import PdfReader, PdfWriter
|
|
10
|
-
from pypdf.generic import DictionaryObject
|
|
11
|
+
from pypdf.generic import ArrayObject, DictionaryObject
|
|
12
|
+
from reportlab.lib.colors import CMYKColor, Color
|
|
11
13
|
|
|
12
14
|
from .constants import (BUTTON_STYLES, DEFAULT_CHECKBOX_STYLE, DEFAULT_FONT,
|
|
13
15
|
DEFAULT_FONT_COLOR, DEFAULT_FONT_SIZE,
|
|
@@ -28,6 +30,22 @@ def stream_to_io(stream: bytes) -> BinaryIO:
|
|
|
28
30
|
return result
|
|
29
31
|
|
|
30
32
|
|
|
33
|
+
def handle_color(color: Union[list, ArrayObject]) -> Union[Color, CMYKColor, None]:
|
|
34
|
+
"""Converts a color array to an RGB or CMYK color."""
|
|
35
|
+
|
|
36
|
+
result = None
|
|
37
|
+
|
|
38
|
+
if len(color) == 1:
|
|
39
|
+
result = CMYKColor(black=1 - color[0])
|
|
40
|
+
elif len(color) == 3:
|
|
41
|
+
result = Color(red=color[0], green=color[1], blue=color[2])
|
|
42
|
+
# write a test case for this
|
|
43
|
+
# elif len(color) == 4:
|
|
44
|
+
# result = CMYKColor(cyan=color[0], magenta=color[1], yellow=color[2], black=color[3])
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
31
49
|
def checkbox_radio_to_draw(
|
|
32
50
|
widget: Union[Checkbox, Radio], font_size: Union[float, int]
|
|
33
51
|
) -> Text:
|
|
@@ -47,17 +65,20 @@ def checkbox_radio_to_draw(
|
|
|
47
65
|
return new_widget
|
|
48
66
|
|
|
49
67
|
|
|
50
|
-
def preview_widget_to_draw(widget: WIDGET_TYPES) -> Text:
|
|
68
|
+
def preview_widget_to_draw(widget: WIDGET_TYPES, with_preview_text: bool) -> Text:
|
|
51
69
|
"""Converts a widget to a preview text widget."""
|
|
52
70
|
|
|
53
71
|
new_widget = Text(
|
|
54
72
|
name=widget.name,
|
|
55
|
-
value="{" + f" {widget.name} " + "}",
|
|
73
|
+
value="{" + f" {widget.name} " + "}" if with_preview_text else None,
|
|
56
74
|
)
|
|
57
75
|
new_widget.font = DEFAULT_FONT
|
|
58
76
|
new_widget.font_size = DEFAULT_FONT_SIZE
|
|
59
77
|
new_widget.font_color = PREVIEW_FONT_COLOR
|
|
60
|
-
new_widget.preview =
|
|
78
|
+
new_widget.preview = with_preview_text
|
|
79
|
+
new_widget.border_color = handle_color([0, 0, 0])
|
|
80
|
+
new_widget.border_width = 1
|
|
81
|
+
new_widget.render_widget = True
|
|
61
82
|
|
|
62
83
|
return new_widget
|
|
63
84
|
|
|
@@ -155,6 +176,25 @@ def traverse_pattern(
|
|
|
155
176
|
return None
|
|
156
177
|
|
|
157
178
|
|
|
179
|
+
def extract_widget_property(
|
|
180
|
+
widget: Union[dict, DictionaryObject],
|
|
181
|
+
patterns: list,
|
|
182
|
+
default_value: Any,
|
|
183
|
+
func_before_return: Union[Callable, None],
|
|
184
|
+
) -> Any:
|
|
185
|
+
"""Returns a property value given a PDF widget dict and a pattern."""
|
|
186
|
+
|
|
187
|
+
result = default_value
|
|
188
|
+
|
|
189
|
+
for pattern in patterns:
|
|
190
|
+
value = traverse_pattern(pattern, widget)
|
|
191
|
+
if value:
|
|
192
|
+
result = func_before_return(value) if func_before_return else value
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
|
|
158
198
|
def generate_unique_suffix() -> str:
|
|
159
199
|
"""Generates a unique suffix string for widgets during form merging."""
|
|
160
200
|
|
PyPDFForm/watermark.py
CHANGED
|
@@ -72,6 +72,36 @@ def draw_text(*args) -> None:
|
|
|
72
72
|
canvas.restoreState()
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def draw_rect(*args) -> None:
|
|
76
|
+
"""Draws a rectangle on the watermark."""
|
|
77
|
+
|
|
78
|
+
canvas = args[0]
|
|
79
|
+
x = args[1]
|
|
80
|
+
y = args[2]
|
|
81
|
+
width = args[3]
|
|
82
|
+
height = args[4]
|
|
83
|
+
|
|
84
|
+
canvas.saveState()
|
|
85
|
+
stroke, fill = set_border_and_background_styles(*args)
|
|
86
|
+
canvas.rect(x, y, width, height, stroke=stroke, fill=fill)
|
|
87
|
+
canvas.restoreState()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def draw_ellipse(*args) -> None:
|
|
91
|
+
"""Draws an ellipse on the watermark."""
|
|
92
|
+
|
|
93
|
+
canvas = args[0]
|
|
94
|
+
x1 = args[1]
|
|
95
|
+
y1 = args[2]
|
|
96
|
+
x2 = args[3]
|
|
97
|
+
y2 = args[4]
|
|
98
|
+
|
|
99
|
+
canvas.saveState()
|
|
100
|
+
stroke, fill = set_border_and_background_styles(*args)
|
|
101
|
+
canvas.ellipse(x1, y1, x2, y2, stroke=stroke, fill=fill)
|
|
102
|
+
canvas.restoreState()
|
|
103
|
+
|
|
104
|
+
|
|
75
105
|
def draw_line(*args) -> None:
|
|
76
106
|
"""Draws a line on the watermark."""
|
|
77
107
|
|
|
@@ -80,26 +110,36 @@ def draw_line(*args) -> None:
|
|
|
80
110
|
src_y = args[2]
|
|
81
111
|
dest_x = args[3]
|
|
82
112
|
dest_y = args[4]
|
|
83
|
-
r = args[5]
|
|
84
|
-
g = args[6]
|
|
85
|
-
b = args[7]
|
|
86
113
|
|
|
87
114
|
canvas.saveState()
|
|
88
|
-
|
|
115
|
+
set_border_and_background_styles(*args)
|
|
89
116
|
canvas.line(src_x, src_y, dest_x, dest_y)
|
|
90
117
|
canvas.restoreState()
|
|
91
118
|
|
|
92
119
|
|
|
93
|
-
def
|
|
94
|
-
"""
|
|
120
|
+
def set_border_and_background_styles(*args) -> tuple:
|
|
121
|
+
"""Sets colors for both border and background before drawing."""
|
|
95
122
|
|
|
96
123
|
canvas = args[0]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
border_color = args[5]
|
|
125
|
+
background_color = args[6]
|
|
126
|
+
border_width = args[7]
|
|
127
|
+
dash_array = args[8]
|
|
101
128
|
|
|
102
|
-
|
|
129
|
+
stroke = 0
|
|
130
|
+
fill = 0
|
|
131
|
+
if border_color is not None and border_width:
|
|
132
|
+
canvas.setStrokeColor(border_color)
|
|
133
|
+
canvas.setLineWidth(border_width)
|
|
134
|
+
stroke = 1
|
|
135
|
+
if background_color is not None:
|
|
136
|
+
canvas.setFillColor(background_color)
|
|
137
|
+
fill = 1
|
|
138
|
+
|
|
139
|
+
if dash_array is not None:
|
|
140
|
+
canvas.setDash(array=dash_array)
|
|
141
|
+
|
|
142
|
+
return stroke, fill
|
|
103
143
|
|
|
104
144
|
|
|
105
145
|
def draw_image(*args) -> None:
|
|
@@ -159,6 +199,9 @@ def create_watermarks_and_draw(
|
|
|
159
199
|
elif action_type == "rect":
|
|
160
200
|
for each in actions:
|
|
161
201
|
draw_rect(*([canvas, *each]))
|
|
202
|
+
elif action_type == "ellipse":
|
|
203
|
+
for each in actions:
|
|
204
|
+
draw_ellipse(*([canvas, *each]))
|
|
162
205
|
|
|
163
206
|
canvas.save()
|
|
164
207
|
buff.seek(0)
|
PyPDFForm/widgets/base.py
CHANGED
|
@@ -10,9 +10,8 @@ from reportlab.lib.colors import Color
|
|
|
10
10
|
from reportlab.pdfgen.canvas import Canvas
|
|
11
11
|
|
|
12
12
|
from ..constants import Annots
|
|
13
|
-
from ..patterns import NON_ACRO_FORM_PARAM_TO_FUNC
|
|
14
|
-
from ..
|
|
15
|
-
from ..utils import stream_to_io
|
|
13
|
+
from ..patterns import NON_ACRO_FORM_PARAM_TO_FUNC, WIDGET_KEY_PATTERNS
|
|
14
|
+
from ..utils import extract_widget_property, stream_to_io
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class Widget:
|
|
@@ -101,7 +100,9 @@ def handle_non_acro_form_params(pdf: bytes, key: str, params: list) -> bytes:
|
|
|
101
100
|
for page in out.pages:
|
|
102
101
|
for annot in page.get(Annots, []): # noqa
|
|
103
102
|
annot = cast(DictionaryObject, annot.get_object())
|
|
104
|
-
_key =
|
|
103
|
+
_key = extract_widget_property(
|
|
104
|
+
annot.get_object(), WIDGET_KEY_PATTERNS, None, str
|
|
105
|
+
)
|
|
105
106
|
|
|
106
107
|
if _key == key:
|
|
107
108
|
for param in params:
|
PyPDFForm/wrapper.py
CHANGED
|
@@ -18,7 +18,7 @@ from .middleware.dropdown import Dropdown
|
|
|
18
18
|
from .middleware.text import Text
|
|
19
19
|
from .template import (build_widgets, dropdown_to_text,
|
|
20
20
|
set_character_x_paddings, update_text_field_attributes,
|
|
21
|
-
update_widget_keys
|
|
21
|
+
update_widget_keys)
|
|
22
22
|
from .utils import (generate_unique_suffix, get_page_streams, merge_two_pdfs,
|
|
23
23
|
preview_widget_to_draw, remove_all_widgets)
|
|
24
24
|
from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
|
|
@@ -52,7 +52,7 @@ class FormWrapper:
|
|
|
52
52
|
) -> FormWrapper:
|
|
53
53
|
"""Fills a PDF form."""
|
|
54
54
|
|
|
55
|
-
widgets = build_widgets(self.stream, False) if self.stream else {}
|
|
55
|
+
widgets = build_widgets(self.stream, False, False) if self.stream else {}
|
|
56
56
|
|
|
57
57
|
for key, value in data.items():
|
|
58
58
|
if key in widgets:
|
|
@@ -72,10 +72,11 @@ class PdfWrapper(FormWrapper):
|
|
|
72
72
|
"""A class to represent a PDF form."""
|
|
73
73
|
|
|
74
74
|
USER_PARAMS = [
|
|
75
|
-
"global_font",
|
|
76
|
-
"global_font_size",
|
|
77
|
-
"global_font_color",
|
|
78
|
-
"use_full_widget_name",
|
|
75
|
+
("global_font", None),
|
|
76
|
+
("global_font_size", None),
|
|
77
|
+
("global_font_color", None),
|
|
78
|
+
("use_full_widget_name", False),
|
|
79
|
+
("render_widgets", True),
|
|
79
80
|
]
|
|
80
81
|
|
|
81
82
|
def __init__(
|
|
@@ -89,8 +90,8 @@ class PdfWrapper(FormWrapper):
|
|
|
89
90
|
self.widgets = {}
|
|
90
91
|
self._keys_to_update = []
|
|
91
92
|
|
|
92
|
-
for
|
|
93
|
-
setattr(self,
|
|
93
|
+
for attr, default in self.USER_PARAMS:
|
|
94
|
+
setattr(self, attr, kwargs.get(attr, default))
|
|
94
95
|
|
|
95
96
|
self._init_helper()
|
|
96
97
|
|
|
@@ -99,7 +100,11 @@ class PdfWrapper(FormWrapper):
|
|
|
99
100
|
|
|
100
101
|
refresh_not_needed = {}
|
|
101
102
|
new_widgets = (
|
|
102
|
-
build_widgets(
|
|
103
|
+
build_widgets(
|
|
104
|
+
self.read(),
|
|
105
|
+
getattr(self, "use_full_widget_name"),
|
|
106
|
+
getattr(self, "render_widgets"),
|
|
107
|
+
)
|
|
103
108
|
if self.read()
|
|
104
109
|
else {}
|
|
105
110
|
)
|
|
@@ -141,7 +146,7 @@ class PdfWrapper(FormWrapper):
|
|
|
141
146
|
|
|
142
147
|
return [
|
|
143
148
|
self.__class__(
|
|
144
|
-
each, **{param: getattr(self, param) for param in self.USER_PARAMS}
|
|
149
|
+
each, **{param: getattr(self, param) for param, _ in self.USER_PARAMS}
|
|
145
150
|
)
|
|
146
151
|
for each in get_page_streams(self.stream)
|
|
147
152
|
]
|
|
@@ -180,15 +185,12 @@ class PdfWrapper(FormWrapper):
|
|
|
180
185
|
"""Inspects all supported widgets' names for the PDF form."""
|
|
181
186
|
|
|
182
187
|
return remove_all_widgets(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
},
|
|
190
|
-
),
|
|
191
|
-
widget_rect_watermarks(self.read()),
|
|
188
|
+
fill(
|
|
189
|
+
self.stream,
|
|
190
|
+
{
|
|
191
|
+
key: preview_widget_to_draw(value, True)
|
|
192
|
+
for key, value in self.widgets.items()
|
|
193
|
+
},
|
|
192
194
|
)
|
|
193
195
|
)
|
|
194
196
|
|
|
@@ -198,8 +200,14 @@ class PdfWrapper(FormWrapper):
|
|
|
198
200
|
"""Inspects a coordinate grid of the PDF."""
|
|
199
201
|
|
|
200
202
|
self.stream = generate_coordinate_grid(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
remove_all_widgets(
|
|
204
|
+
fill(
|
|
205
|
+
self.stream,
|
|
206
|
+
{
|
|
207
|
+
key: preview_widget_to_draw(value, False)
|
|
208
|
+
for key, value in self.widgets.items()
|
|
209
|
+
},
|
|
210
|
+
)
|
|
203
211
|
),
|
|
204
212
|
color,
|
|
205
213
|
margin,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPDFForm
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: The Python library for PDF forms.
|
|
5
5
|
Home-page: https://github.com/chinapandaman/PyPDFForm
|
|
6
6
|
Author: Jinge Li
|
|
@@ -29,6 +29,7 @@ Dynamic: classifier
|
|
|
29
29
|
Dynamic: description
|
|
30
30
|
Dynamic: description-content-type
|
|
31
31
|
Dynamic: home-page
|
|
32
|
+
Dynamic: license-file
|
|
32
33
|
Dynamic: requires-dist
|
|
33
34
|
Dynamic: requires-python
|
|
34
35
|
Dynamic: summary
|
|
@@ -43,6 +44,15 @@ Dynamic: summary
|
|
|
43
44
|
<a href="https://pypistats.org/packages/pypdfform"><img src="https://img.shields.io/pypi/dm/pypdfform?logo=pypi&logoColor=white&label=downloads&labelColor=black&color=blue&style=for-the-badge"></a>
|
|
44
45
|
</p>
|
|
45
46
|
|
|
47
|
+
## Important Announcements
|
|
48
|
+
|
|
49
|
+
Hello fellow Python developers! With the release of v2.0.0, there are some important changes I'm making to the library:
|
|
50
|
+
|
|
51
|
+
* Since I started developing the library, versioning releases has been quite unorthodox, and there isn't any convention I followed. Starting with v2.0.0, PyPDFForm will version releases following the conventions defined by [Semantic Versioning](https://semver.org/).
|
|
52
|
+
* PyPDFForm now renders PDF form widgets! Ever since its ancestral stage, the library has only been able to render the data you filled into a PDF form. Now if you fill a PDF form using `PdfWrapper`, the result will render the whole widget instead of just the value that got filled. If you would like to disable this behavior, please refer to the docs [here](https://chinapandaman.github.io/PyPDFForm/fill/#disable-rendering-widgets).
|
|
53
|
+
|
|
54
|
+
Happy hacking!
|
|
55
|
+
|
|
46
56
|
## Introduction
|
|
47
57
|
|
|
48
58
|
PyPDFForm is a free and open source pure-Python 3 library for PDF form processing. It contains the essential
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
PyPDFForm/__init__.py,sha256=CtFNtHaoeqe0pQK6LTIHuWlPop37g6DGqsfPmxnqyUA,178
|
|
2
|
+
PyPDFForm/adapter.py,sha256=qt0lqd7zRzaMHD2As47oiIhungO3AyWbAkxx1ZAZi9A,862
|
|
3
|
+
PyPDFForm/constants.py,sha256=2ti8lDF2K965jfom3l9jN_5cKxjEYyQNjr1VQlYvpNU,1869
|
|
4
|
+
PyPDFForm/coordinate.py,sha256=FtVkpmBYtBfwDLkr5o2VxifaWvEV8MKbRoTdLrcPHR0,9082
|
|
5
|
+
PyPDFForm/filler.py,sha256=oxSDZWsaXLI085W1ALEqBRuKS0tBbunoX1WzEztMYBk,9765
|
|
6
|
+
PyPDFForm/font.py,sha256=RHTZAYHfmIZedeNZwNlSg8aNUCyBsJA0t_hhHiV9GoQ,5566
|
|
7
|
+
PyPDFForm/image.py,sha256=IsmT2_LAvoGTxbsrelXnP8B4vUKetOVwp6oiE9MGnU8,835
|
|
8
|
+
PyPDFForm/patterns.py,sha256=X8lTlpn2FxHDKLgNy3O2PzKIcx9wF2FziI1JGC2Z-cg,5708
|
|
9
|
+
PyPDFForm/template.py,sha256=IQDLhFt7sVWNaY0SRRosWIk_nvMVFDex7N7RGZsQcvE,15197
|
|
10
|
+
PyPDFForm/utils.py,sha256=hdj-0nP3t2UBZnEUB74W_WMPxHNN2SpZ1MyMHar90bQ,6013
|
|
11
|
+
PyPDFForm/watermark.py,sha256=m3PXsNGJdW_DWKegHtQmWjCcDEbFiDPw_wyL_GNyKLI,5926
|
|
12
|
+
PyPDFForm/wrapper.py,sha256=zzf2VkfkviA51dKiVSkCHgCUwBC4yxADOpvb_VRLbtI,11909
|
|
13
|
+
PyPDFForm/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
PyPDFForm/middleware/base.py,sha256=cfCzpbFkvCxQlFwmZ8BV-HiJ_8YhXvbBp45kUWEudQs,1351
|
|
15
|
+
PyPDFForm/middleware/checkbox.py,sha256=GQPaKlPo6IX14N4tLLNf2hyJZBZmERs_9cWiwhvLg0Q,1376
|
|
16
|
+
PyPDFForm/middleware/dropdown.py,sha256=_RXs9o3vWH_a402zhTOYE_mcWbtFfVU7bReYcEFAIyk,793
|
|
17
|
+
PyPDFForm/middleware/image.py,sha256=tQrLKZUKOz90jKxuUQpTP4Awuvtf0I92LeANH0k6mzQ,212
|
|
18
|
+
PyPDFForm/middleware/radio.py,sha256=4Twv2uOW_GhVV7PHR3RFm4SDyGBEKqvL1cqj7i_Zi7Q,891
|
|
19
|
+
PyPDFForm/middleware/signature.py,sha256=1ZOkBKectZ7kptJMQ-EuOA_xI7j8dlwseo2p2Z0FS2o,1138
|
|
20
|
+
PyPDFForm/middleware/text.py,sha256=7HFHy_lfBFfYbSUzgVKovSoShbNP6L7tiILIw-9_kpo,1549
|
|
21
|
+
PyPDFForm/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
PyPDFForm/widgets/base.py,sha256=2v0y3vb5Dqy8G4oLm-xHkqi6GNWV1nnL8GEIe8DEU0c,3427
|
|
23
|
+
PyPDFForm/widgets/checkbox.py,sha256=MGB6NGI-XMBz4j2erykgYOCd1pswvthuQ6UFowQOd4o,503
|
|
24
|
+
PyPDFForm/widgets/dropdown.py,sha256=2_R5xcLUWAL0G4raVgWWtE7TJdiWJITay-Gtl8YI2QI,705
|
|
25
|
+
PyPDFForm/widgets/text.py,sha256=sCB8CQAjh30QOGr8FTIDSk3X6dXSHJztOz6YkKEBDss,691
|
|
26
|
+
pypdfform-2.0.0.dist-info/licenses/LICENSE,sha256=43awmYkI6opyTpg19me731iO1WfXZwViqb67oWtCsFY,1065
|
|
27
|
+
pypdfform-2.0.0.dist-info/METADATA,sha256=rUiD5ajgngBZ8crGRl488hnleb0ah4HOgytdKk6vMs4,5265
|
|
28
|
+
pypdfform-2.0.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
29
|
+
pypdfform-2.0.0.dist-info/top_level.txt,sha256=GQQKuWqPUjT9YZqwK95NlAQzxjwoQrsxQ8ureM8lWOY,10
|
|
30
|
+
pypdfform-2.0.0.dist-info/RECORD,,
|
pypdfform-1.5.7.dist-info/RECORD
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
PyPDFForm/__init__.py,sha256=rnGnnQOhm4df5ycwLI_K3wFB0pxer6koep5nLg_bnQU,178
|
|
2
|
-
PyPDFForm/adapter.py,sha256=qt0lqd7zRzaMHD2As47oiIhungO3AyWbAkxx1ZAZi9A,862
|
|
3
|
-
PyPDFForm/constants.py,sha256=iHCwbnypwgMe5T_hMZIJJixySkOlXgtXuNlI2UUptJ8,1775
|
|
4
|
-
PyPDFForm/coordinate.py,sha256=uZGGFiTUIrD9VX-bzgUpKlFAzK7VJppW1corGXvT-pY,7712
|
|
5
|
-
PyPDFForm/filler.py,sha256=BWj3CpRWTrNCNBrMMptdxW9Nac6pnQCl3xOw5l0QPy8,7597
|
|
6
|
-
PyPDFForm/font.py,sha256=TbFLY4G72d7cQ3VG2g4HaYKROzQPQUmh6-8ynN6hzXY,5713
|
|
7
|
-
PyPDFForm/image.py,sha256=IsmT2_LAvoGTxbsrelXnP8B4vUKetOVwp6oiE9MGnU8,835
|
|
8
|
-
PyPDFForm/patterns.py,sha256=Z548TjpQpaVepctvlRYe0-2i7Aws96fo-ncG82CUX14,5423
|
|
9
|
-
PyPDFForm/template.py,sha256=vvXPUkHm8WvbMBpvi6H4V3ZgvCdPtpAlJ9rDCVQUxQg,15541
|
|
10
|
-
PyPDFForm/utils.py,sha256=hMWGLyTZJSQG4gItlnQLq8RxnV4bM2sL6_-RmLqSwtE,4721
|
|
11
|
-
PyPDFForm/watermark.py,sha256=eDGQKXc4j1uUhAAE1VeQXGHQ9FO4h9mSKExv3gnNTZg,4746
|
|
12
|
-
PyPDFForm/wrapper.py,sha256=fxvwP-bGqq7yAFjUr4PtHBUVU_f0cdrQMtmamOPrk5s,11716
|
|
13
|
-
PyPDFForm/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
PyPDFForm/middleware/base.py,sha256=Sjo8O9RrdBu73r4BCMPuLXnrl1hzjb1dlTOPeIgrhOU,1150
|
|
15
|
-
PyPDFForm/middleware/checkbox.py,sha256=0EWFECXYdz9SNxgGe1QCcRrdxLVUIJpzRYwKfGRA9e4,1346
|
|
16
|
-
PyPDFForm/middleware/dropdown.py,sha256=_RXs9o3vWH_a402zhTOYE_mcWbtFfVU7bReYcEFAIyk,793
|
|
17
|
-
PyPDFForm/middleware/image.py,sha256=tQrLKZUKOz90jKxuUQpTP4Awuvtf0I92LeANH0k6mzQ,212
|
|
18
|
-
PyPDFForm/middleware/radio.py,sha256=KfwOtlnMk5B2y8KbqD-ONN5AbkHa4TF5NzdcZlbNyDk,598
|
|
19
|
-
PyPDFForm/middleware/signature.py,sha256=1ZOkBKectZ7kptJMQ-EuOA_xI7j8dlwseo2p2Z0FS2o,1138
|
|
20
|
-
PyPDFForm/middleware/text.py,sha256=7HFHy_lfBFfYbSUzgVKovSoShbNP6L7tiILIw-9_kpo,1549
|
|
21
|
-
PyPDFForm/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
PyPDFForm/widgets/base.py,sha256=nb6HEfRdWSjZc_Th1XI62PIlgREn_JiApGC-epb6jAg,3348
|
|
23
|
-
PyPDFForm/widgets/checkbox.py,sha256=MGB6NGI-XMBz4j2erykgYOCd1pswvthuQ6UFowQOd4o,503
|
|
24
|
-
PyPDFForm/widgets/dropdown.py,sha256=2_R5xcLUWAL0G4raVgWWtE7TJdiWJITay-Gtl8YI2QI,705
|
|
25
|
-
PyPDFForm/widgets/text.py,sha256=sCB8CQAjh30QOGr8FTIDSk3X6dXSHJztOz6YkKEBDss,691
|
|
26
|
-
pypdfform-1.5.7.dist-info/LICENSE,sha256=43awmYkI6opyTpg19me731iO1WfXZwViqb67oWtCsFY,1065
|
|
27
|
-
pypdfform-1.5.7.dist-info/METADATA,sha256=E2qQcC1PGsUdD_M7rUSqxPimQAlR-tpDpakRe6iGSQQ,4376
|
|
28
|
-
pypdfform-1.5.7.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
29
|
-
pypdfform-1.5.7.dist-info/top_level.txt,sha256=GQQKuWqPUjT9YZqwK95NlAQzxjwoQrsxQ8ureM8lWOY,10
|
|
30
|
-
pypdfform-1.5.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|