PyPDFForm 2.0.1__tar.gz → 2.1.0__tar.gz
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-2.0.1 → pypdfform-2.1.0}/PKG-INFO +1 -1
- pypdfform-2.1.0/PyPDFForm/__init__.py +12 -0
- pypdfform-2.1.0/PyPDFForm/adapter.py +66 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/constants.py +12 -1
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/coordinate.py +218 -59
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/filler.py +104 -9
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/font.py +80 -16
- pypdfform-2.1.0/PyPDFForm/image.py +69 -0
- pypdfform-2.1.0/PyPDFForm/middleware/base.py +111 -0
- pypdfform-2.1.0/PyPDFForm/middleware/checkbox.py +97 -0
- pypdfform-2.1.0/PyPDFForm/middleware/dropdown.py +72 -0
- pypdfform-2.1.0/PyPDFForm/middleware/image.py +34 -0
- pypdfform-2.1.0/PyPDFForm/middleware/radio.py +73 -0
- pypdfform-2.1.0/PyPDFForm/middleware/signature.py +88 -0
- pypdfform-2.1.0/PyPDFForm/middleware/text.py +112 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/patterns.py +108 -10
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/template.py +181 -29
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/utils.py +108 -12
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/watermark.py +151 -9
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/widgets/base.py +65 -9
- pypdfform-2.1.0/PyPDFForm/widgets/checkbox.py +39 -0
- pypdfform-2.1.0/PyPDFForm/widgets/dropdown.py +56 -0
- pypdfform-2.1.0/PyPDFForm/widgets/radio.py +78 -0
- pypdfform-2.1.0/PyPDFForm/widgets/text.py +41 -0
- pypdfform-2.1.0/PyPDFForm/wrapper.py +704 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm.egg-info/PKG-INFO +1 -1
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm.egg-info/SOURCES.txt +1 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_create_widget.py +74 -2
- pypdfform-2.0.1/PyPDFForm/__init__.py +0 -8
- pypdfform-2.0.1/PyPDFForm/adapter.py +0 -32
- pypdfform-2.0.1/PyPDFForm/image.py +0 -40
- pypdfform-2.0.1/PyPDFForm/middleware/base.py +0 -62
- pypdfform-2.0.1/PyPDFForm/middleware/checkbox.py +0 -55
- pypdfform-2.0.1/PyPDFForm/middleware/dropdown.py +0 -36
- pypdfform-2.0.1/PyPDFForm/middleware/image.py +0 -10
- pypdfform-2.0.1/PyPDFForm/middleware/radio.py +0 -37
- pypdfform-2.0.1/PyPDFForm/middleware/signature.py +0 -45
- pypdfform-2.0.1/PyPDFForm/middleware/text.py +0 -64
- pypdfform-2.0.1/PyPDFForm/widgets/checkbox.py +0 -19
- pypdfform-2.0.1/PyPDFForm/widgets/dropdown.py +0 -28
- pypdfform-2.0.1/PyPDFForm/widgets/text.py +0 -24
- pypdfform-2.0.1/PyPDFForm/wrapper.py +0 -393
- {pypdfform-2.0.1 → pypdfform-2.1.0}/LICENSE +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/middleware/__init__.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm/widgets/__init__.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm.egg-info/requires.txt +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/README.md +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/setup.cfg +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/setup.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_adobe_mode.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_dropdown.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_dropdown_simple.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_fill_max_length_text_field_simple.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_fill_method.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_functional.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_functional_simple.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_paragraph.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_paragraph_simple.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_preview.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_signature.py +0 -0
- {pypdfform-2.0.1 → pypdfform-2.1.0}/tests/test_use_full_widget_name.py +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""PyPDFForm package for PDF form filling and manipulation.
|
|
3
|
+
|
|
4
|
+
This package provides tools for filling PDF forms, drawing text and images,
|
|
5
|
+
and manipulating PDF form elements programmatically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "2.1.0"
|
|
9
|
+
|
|
10
|
+
from .wrapper import FormWrapper, PdfWrapper
|
|
11
|
+
|
|
12
|
+
__all__ = ["FormWrapper", "PdfWrapper"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Provides adapters for handling different types of file inputs.
|
|
3
|
+
|
|
4
|
+
This module contains utility functions for working with various file input types:
|
|
5
|
+
- Raw bytes
|
|
6
|
+
- File paths
|
|
7
|
+
- File-like objects
|
|
8
|
+
|
|
9
|
+
The adapters normalize these different input types into consistent byte streams
|
|
10
|
+
that can be processed by the PDF manipulation functions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from os.path import isfile
|
|
14
|
+
from typing import Any, BinaryIO, Union
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def readable(obj: Any) -> bool:
|
|
18
|
+
"""Determines if an object is file-like and readable.
|
|
19
|
+
|
|
20
|
+
Checks if the object has a callable read() method, indicating it can be
|
|
21
|
+
treated as a file-like object for reading operations.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
obj: The object to check for read capability
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
bool: True if the object has a callable read() method, False otherwise
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
return callable(getattr(obj, "read", None))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fp_or_f_obj_or_stream_to_stream(
|
|
34
|
+
fp_or_f_obj_or_stream: Union[bytes, str, BinaryIO],
|
|
35
|
+
) -> bytes:
|
|
36
|
+
"""Converts various file input types to a byte stream.
|
|
37
|
+
|
|
38
|
+
Handles conversion of:
|
|
39
|
+
- Raw bytes (passed through unchanged)
|
|
40
|
+
- File paths (reads file contents)
|
|
41
|
+
- File-like objects (reads using read() method)
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
fp_or_f_obj_or_stream: Input to convert, which can be:
|
|
45
|
+
- bytes: Raw PDF data
|
|
46
|
+
- str: Path to PDF file
|
|
47
|
+
- BinaryIO: File-like object containing PDF data
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
bytes: The PDF data as a byte stream
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
result = b""
|
|
54
|
+
if isinstance(fp_or_f_obj_or_stream, bytes):
|
|
55
|
+
result = fp_or_f_obj_or_stream
|
|
56
|
+
|
|
57
|
+
elif readable(fp_or_f_obj_or_stream):
|
|
58
|
+
result = fp_or_f_obj_or_stream.read()
|
|
59
|
+
|
|
60
|
+
elif isinstance(fp_or_f_obj_or_stream, str):
|
|
61
|
+
if not isfile(fp_or_f_obj_or_stream):
|
|
62
|
+
pass
|
|
63
|
+
else:
|
|
64
|
+
with open(fp_or_f_obj_or_stream, "rb+") as _file:
|
|
65
|
+
result = _file.read()
|
|
66
|
+
return result
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
2
|
+
"""Centralized location for all PyPDFForm constants and default values.
|
|
3
|
+
|
|
4
|
+
This module defines all the constants used throughout the package including:
|
|
5
|
+
- PDF version identifiers and format strings
|
|
6
|
+
- PDF field type and property constants
|
|
7
|
+
- Default values for fonts, colors and sizes
|
|
8
|
+
- Special identifiers and symbols
|
|
9
|
+
- Field flag bitmasks
|
|
10
|
+
- Button style definitions
|
|
11
|
+
|
|
12
|
+
All constants are organized by category and used consistently across the package.
|
|
13
|
+
"""
|
|
3
14
|
|
|
4
15
|
from typing import Union
|
|
5
16
|
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
2
|
+
"""Provides coordinate calculation utilities for PDF form elements.
|
|
3
|
+
|
|
4
|
+
This module contains functions for calculating positions and dimensions
|
|
5
|
+
for drawing various PDF form elements including:
|
|
6
|
+
- Text fields and paragraphs
|
|
7
|
+
- Checkboxes and radio buttons
|
|
8
|
+
- Images and signatures
|
|
9
|
+
- Borders and decorative elements
|
|
10
|
+
|
|
11
|
+
All calculations work in PDF coordinate space where:
|
|
12
|
+
- Origin (0,0) is at bottom-left corner
|
|
13
|
+
- Units are in PDF points (1/72 inch)
|
|
14
|
+
"""
|
|
3
15
|
|
|
4
16
|
from copy import deepcopy
|
|
5
17
|
from typing import List, Tuple, Union
|
|
@@ -17,7 +29,17 @@ from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
|
|
|
17
29
|
|
|
18
30
|
|
|
19
31
|
def get_draw_border_coordinates(widget: dict, shape: str) -> List[float]:
|
|
20
|
-
"""
|
|
32
|
+
"""Calculates coordinates for drawing widget borders.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
widget: PDF form widget dictionary containing Rect coordinates
|
|
36
|
+
shape: Type of border to draw ("rectangle", "ellipse" or "line")
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List[float]: Coordinates in format [x1, y1, x2, y2] for the border
|
|
40
|
+
For ellipses: [center_x, center_y, radius_x, radius_y]
|
|
41
|
+
For lines: [x1, y1, x2, y2] endpoints
|
|
42
|
+
"""
|
|
21
43
|
|
|
22
44
|
result = [
|
|
23
45
|
float(widget[Rect][0]),
|
|
@@ -57,7 +79,17 @@ def get_draw_checkbox_radio_coordinates(
|
|
|
57
79
|
widget_middleware: Text,
|
|
58
80
|
border_width: int,
|
|
59
81
|
) -> Tuple[Union[float, int], Union[float, int]]:
|
|
60
|
-
"""
|
|
82
|
+
"""Calculates drawing coordinates for checkbox/radio button symbols.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
widget: PDF form widget dictionary containing Rect coordinates
|
|
86
|
+
widget_middleware: Text middleware containing font properties
|
|
87
|
+
border_width: Width of widget border in points
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tuple[Union[float, int], Union[float, int]]: (x, y) coordinates
|
|
91
|
+
for drawing the checkbox/radio symbol
|
|
92
|
+
"""
|
|
61
93
|
|
|
62
94
|
string_height = widget_middleware.font_size * 72 / 96
|
|
63
95
|
width_mid_point = (float(widget[Rect][0]) + float(widget[Rect][2])) / 2
|
|
@@ -83,8 +115,18 @@ def get_draw_image_coordinates_resolutions(
|
|
|
83
115
|
image_width: float,
|
|
84
116
|
image_height: float,
|
|
85
117
|
) -> Tuple[float, float, float, float]:
|
|
86
|
-
"""
|
|
87
|
-
|
|
118
|
+
"""Calculates image drawing coordinates and scaling factors.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
widget: PDF form widget dictionary containing Rect coordinates
|
|
122
|
+
preserve_aspect_ratio: Whether to maintain image proportions
|
|
123
|
+
image_width: Original width of the image in points
|
|
124
|
+
image_height: Original height of the image in points
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Tuple[float, float, float, float]: (x, y, width, height) where:
|
|
128
|
+
x,y: Bottom-left corner coordinates
|
|
129
|
+
width,height: Scaled dimensions for drawing
|
|
88
130
|
"""
|
|
89
131
|
|
|
90
132
|
x = float(widget[Rect][0])
|
|
@@ -107,17 +149,161 @@ def get_draw_image_coordinates_resolutions(
|
|
|
107
149
|
return x, y, width, height
|
|
108
150
|
|
|
109
151
|
|
|
152
|
+
def calculate_text_coord_x(
|
|
153
|
+
widget: dict,
|
|
154
|
+
widget_middleware: Text,
|
|
155
|
+
text_value: str,
|
|
156
|
+
length: int,
|
|
157
|
+
character_paddings: List[float],
|
|
158
|
+
) -> float:
|
|
159
|
+
"""
|
|
160
|
+
Calculate the horizontal (x) coordinate for text drawing within a PDF form field.
|
|
161
|
+
|
|
162
|
+
This function determines the x-coordinate based on:
|
|
163
|
+
- The widget's alignment setting (left, center, right)
|
|
164
|
+
- Whether the field uses comb formatting
|
|
165
|
+
- The width of the text string
|
|
166
|
+
- Character paddings for comb fields
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
widget: PDF form widget dictionary containing Rect and alignment info.
|
|
170
|
+
widget_middleware: Text middleware containing font and comb properties.
|
|
171
|
+
text_value: The text string to be drawn (already trimmed).
|
|
172
|
+
length: The length of the text string.
|
|
173
|
+
character_paddings: List of cumulative paddings for comb characters.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
float: The calculated x-coordinate for the text baseline start.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
alignment = (
|
|
180
|
+
extract_widget_property(widget, WIDGET_ALIGNMENT_PATTERNS, None, int) or 0
|
|
181
|
+
)
|
|
182
|
+
# Default to left boundary
|
|
183
|
+
x = float(widget[Rect][0])
|
|
184
|
+
|
|
185
|
+
if int(alignment) == 0:
|
|
186
|
+
return x
|
|
187
|
+
|
|
188
|
+
# Calculate the horizontal midpoint of the widget rectangle
|
|
189
|
+
width_mid_point = (float(widget[Rect][0]) + float(widget[Rect][2])) / 2
|
|
190
|
+
|
|
191
|
+
# Calculate the width of the entire string in points
|
|
192
|
+
string_width = stringWidth(
|
|
193
|
+
text_value,
|
|
194
|
+
widget_middleware.font,
|
|
195
|
+
widget_middleware.font_size,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# If comb formatting, adjust string width to include last character's right padding
|
|
199
|
+
if widget_middleware.comb is True and length:
|
|
200
|
+
string_width = character_paddings[-1] + stringWidth(
|
|
201
|
+
text_value[-1],
|
|
202
|
+
widget_middleware.font,
|
|
203
|
+
widget_middleware.font_size,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if int(alignment) == 1: # Center alignment
|
|
207
|
+
# Center text by offsetting half the string width from the midpoint
|
|
208
|
+
x = width_mid_point - string_width / 2
|
|
209
|
+
elif int(alignment) == 2: # Right alignment
|
|
210
|
+
# Align text to the right edge minus the string width
|
|
211
|
+
x = float(widget[Rect][2]) - string_width
|
|
212
|
+
if length > 0 and widget_middleware.comb is True:
|
|
213
|
+
# For comb fields, adjust further by half the difference between comb box width and last character width
|
|
214
|
+
x -= (
|
|
215
|
+
get_char_rect_width(widget, widget_middleware)
|
|
216
|
+
- stringWidth(
|
|
217
|
+
text_value[-1],
|
|
218
|
+
widget_middleware.font,
|
|
219
|
+
widget_middleware.font_size,
|
|
220
|
+
)
|
|
221
|
+
) / 2
|
|
222
|
+
|
|
223
|
+
# Additional comb adjustment for center alignment
|
|
224
|
+
if int(alignment) == 1 and widget_middleware.comb is True and length != 0:
|
|
225
|
+
# Shift left by half the first character's padding
|
|
226
|
+
x -= character_paddings[0] / 2
|
|
227
|
+
if length % 2 == 0:
|
|
228
|
+
# For even-length comb text, shift further left by half the first char width plus padding
|
|
229
|
+
x -= (
|
|
230
|
+
character_paddings[0]
|
|
231
|
+
+ stringWidth(
|
|
232
|
+
text_value[:1],
|
|
233
|
+
widget_middleware.font,
|
|
234
|
+
widget_middleware.font_size,
|
|
235
|
+
)
|
|
236
|
+
/ 2
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return x
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def calculate_text_coord_y(widget: dict, widget_middleware: Text) -> float:
|
|
243
|
+
"""
|
|
244
|
+
Calculate the vertical (y) coordinate for text drawing within a PDF form field.
|
|
245
|
+
|
|
246
|
+
This function determines the y-coordinate based on:
|
|
247
|
+
- The widget's rectangle height
|
|
248
|
+
- The font size
|
|
249
|
+
- Whether the field is multiline (which shifts text closer to the top)
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
widget: PDF form widget dictionary containing Rect info.
|
|
253
|
+
widget_middleware: Text middleware containing font size and multiline info.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
float: The calculated y-coordinate for the text baseline.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
# Convert font size to PDF points (font size is in pixels, 96 dpi to 72 dpi)
|
|
260
|
+
string_height = widget_middleware.font_size * 96 / 72
|
|
261
|
+
|
|
262
|
+
# Calculate vertical midpoint of the widget rectangle
|
|
263
|
+
height_mid_point = (float(widget[Rect][1]) + float(widget[Rect][3])) / 2
|
|
264
|
+
|
|
265
|
+
# Default y: vertically center the text baseline within the widget
|
|
266
|
+
# This formula centers the text height around the vertical midpoint
|
|
267
|
+
y = (height_mid_point - string_height / 2 + height_mid_point) / 2
|
|
268
|
+
|
|
269
|
+
# If multiline, position baseline closer to the top edge for better appearance
|
|
270
|
+
if is_text_multiline(widget):
|
|
271
|
+
y = float(widget[Rect][3]) - string_height / 1.5
|
|
272
|
+
|
|
273
|
+
return y
|
|
274
|
+
|
|
275
|
+
|
|
110
276
|
def get_draw_text_coordinates(
|
|
111
277
|
widget: dict, widget_middleware: Text
|
|
112
278
|
) -> Tuple[Union[float, int], Union[float, int]]:
|
|
113
|
-
"""
|
|
279
|
+
"""
|
|
280
|
+
Calculate the (x, y) coordinates for drawing text within a PDF form field.
|
|
281
|
+
|
|
282
|
+
This function determines the starting position for rendering text,
|
|
283
|
+
taking into account:
|
|
284
|
+
- Preview mode (which offsets text above the field)
|
|
285
|
+
- Text length and wrapping
|
|
286
|
+
- Alignment (left, center, right)
|
|
287
|
+
- Comb formatting and character paddings
|
|
288
|
+
- Multiline adjustments for vertical position
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
widget: PDF form widget dictionary containing Rect and alignment info.
|
|
292
|
+
widget_middleware: Text middleware containing font, comb, and text properties.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Tuple[Union[float, int], Union[float, int]]: The (x, y) coordinates
|
|
296
|
+
for the text baseline starting point.
|
|
297
|
+
"""
|
|
114
298
|
|
|
299
|
+
# If preview mode, draw slightly above the top boundary for visibility
|
|
115
300
|
if widget_middleware.preview:
|
|
116
301
|
return (
|
|
117
302
|
float(widget[Rect][0]),
|
|
118
303
|
float(widget[Rect][3]) + 5,
|
|
119
304
|
)
|
|
120
305
|
|
|
306
|
+
# Prepare text value, respecting max length
|
|
121
307
|
text_value = widget_middleware.value or ""
|
|
122
308
|
length = (
|
|
123
309
|
min(len(text_value), widget_middleware.max_length)
|
|
@@ -126,66 +312,24 @@ def get_draw_text_coordinates(
|
|
|
126
312
|
)
|
|
127
313
|
text_value = text_value[:length]
|
|
128
314
|
|
|
315
|
+
# Further trim text if wrapping is enabled
|
|
129
316
|
if widget_middleware.text_wrap_length is not None:
|
|
130
317
|
text_value = text_value[: widget_middleware.text_wrap_length]
|
|
131
318
|
|
|
319
|
+
# Prepare character paddings for comb fields
|
|
132
320
|
character_paddings = (
|
|
133
321
|
widget_middleware.character_paddings[:length]
|
|
134
322
|
if widget_middleware.character_paddings is not None
|
|
135
323
|
else widget_middleware.character_paddings
|
|
136
324
|
)
|
|
137
325
|
|
|
138
|
-
alignment
|
|
139
|
-
|
|
326
|
+
# Calculate horizontal position based on alignment and comb settings
|
|
327
|
+
x = calculate_text_coord_x(
|
|
328
|
+
widget, widget_middleware, text_value, length, character_paddings
|
|
140
329
|
)
|
|
141
|
-
x = float(widget[Rect][0])
|
|
142
|
-
|
|
143
|
-
if int(alignment) != 0:
|
|
144
|
-
width_mid_point = (float(widget[Rect][0]) + float(widget[Rect][2])) / 2
|
|
145
|
-
string_width = stringWidth(
|
|
146
|
-
text_value,
|
|
147
|
-
widget_middleware.font,
|
|
148
|
-
widget_middleware.font_size,
|
|
149
|
-
)
|
|
150
|
-
if widget_middleware.comb is True and length:
|
|
151
|
-
string_width = character_paddings[-1] + stringWidth(
|
|
152
|
-
text_value[-1],
|
|
153
|
-
widget_middleware.font,
|
|
154
|
-
widget_middleware.font_size,
|
|
155
|
-
)
|
|
156
330
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
elif int(alignment) == 2:
|
|
160
|
-
x = float(widget[Rect][2]) - string_width
|
|
161
|
-
if length > 0 and widget_middleware.comb is True:
|
|
162
|
-
x -= (
|
|
163
|
-
get_char_rect_width(widget, widget_middleware)
|
|
164
|
-
- stringWidth(
|
|
165
|
-
text_value[-1],
|
|
166
|
-
widget_middleware.font,
|
|
167
|
-
widget_middleware.font_size,
|
|
168
|
-
)
|
|
169
|
-
) / 2
|
|
170
|
-
|
|
171
|
-
string_height = widget_middleware.font_size * 96 / 72
|
|
172
|
-
height_mid_point = (float(widget[Rect][1]) + float(widget[Rect][3])) / 2
|
|
173
|
-
y = (height_mid_point - string_height / 2 + height_mid_point) / 2
|
|
174
|
-
if is_text_multiline(widget):
|
|
175
|
-
y = float(widget[Rect][3]) - string_height / 1.5
|
|
176
|
-
|
|
177
|
-
if int(alignment) == 1 and widget_middleware.comb is True and length != 0:
|
|
178
|
-
x -= character_paddings[0] / 2
|
|
179
|
-
if length % 2 == 0:
|
|
180
|
-
x -= (
|
|
181
|
-
character_paddings[0]
|
|
182
|
-
+ stringWidth(
|
|
183
|
-
text_value[:1],
|
|
184
|
-
widget_middleware.font,
|
|
185
|
-
widget_middleware.font_size,
|
|
186
|
-
)
|
|
187
|
-
/ 2
|
|
188
|
-
)
|
|
331
|
+
# Calculate vertical position based on font size and multiline
|
|
332
|
+
y = calculate_text_coord_y(widget, widget_middleware)
|
|
189
333
|
|
|
190
334
|
return x, y
|
|
191
335
|
|
|
@@ -193,9 +337,15 @@ def get_draw_text_coordinates(
|
|
|
193
337
|
def get_text_line_x_coordinates(
|
|
194
338
|
widget: dict, widget_middleware: Text
|
|
195
339
|
) -> Union[List[float], None]:
|
|
196
|
-
"""
|
|
197
|
-
|
|
198
|
-
|
|
340
|
+
"""Calculates x-coordinates for each line in a multiline text field.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
widget: PDF form widget dictionary
|
|
344
|
+
widget_middleware: Text middleware with text_lines property
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Union[List[float], None]: List of x-coordinates for each text line,
|
|
348
|
+
or None if not a multiline field
|
|
199
349
|
"""
|
|
200
350
|
|
|
201
351
|
if (
|
|
@@ -220,7 +370,16 @@ def get_text_line_x_coordinates(
|
|
|
220
370
|
def generate_coordinate_grid(
|
|
221
371
|
pdf: bytes, color: Tuple[float, float, float], margin: float
|
|
222
372
|
) -> bytes:
|
|
223
|
-
"""
|
|
373
|
+
"""Generates a coordinate grid overlay for a PDF document.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
pdf: Input PDF document as bytes
|
|
377
|
+
color: RGB tuple (0-1 range) for grid line color
|
|
378
|
+
margin: Spacing between grid lines in PDF points
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
bytes: New PDF with grid overlay as byte stream
|
|
382
|
+
"""
|
|
224
383
|
|
|
225
384
|
pdf_file = PdfReader(stream_to_io(pdf))
|
|
226
385
|
lines_by_page = {}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
2
|
+
"""Provides core functionality for filling PDF form fields.
|
|
3
|
+
|
|
4
|
+
This module handles:
|
|
5
|
+
- Drawing text, images, borders and other elements onto PDF forms
|
|
6
|
+
- Managing widget states and appearances
|
|
7
|
+
- Supporting different filling modes (simple vs watermark-based)
|
|
8
|
+
- Handling special cases like checkboxes, radio buttons and signatures
|
|
9
|
+
|
|
10
|
+
The main functions are:
|
|
11
|
+
- fill(): Uses watermark technique for complex form filling
|
|
12
|
+
- simple_fill(): Directly modifies form fields for simpler cases
|
|
13
|
+
"""
|
|
3
14
|
|
|
4
15
|
from io import BytesIO
|
|
5
16
|
from typing import Dict, Tuple, Union, cast
|
|
@@ -35,7 +46,20 @@ from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
|
|
|
35
46
|
def check_radio_handler(
|
|
36
47
|
widget: dict, middleware: Union[Checkbox, Radio], radio_button_tracker: dict
|
|
37
48
|
) -> Tuple[Text, Union[float, int], Union[float, int], bool]:
|
|
38
|
-
"""
|
|
49
|
+
"""Calculates drawing parameters for checkbox and radio button widgets.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
widget: PDF form widget dictionary containing Rect coordinates
|
|
53
|
+
middleware: Checkbox or Radio middleware instance
|
|
54
|
+
radio_button_tracker: Dictionary tracking radio button group states
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tuple containing:
|
|
58
|
+
- Text: Prepared text object for drawing the symbol
|
|
59
|
+
- float/int: x coordinate for drawing
|
|
60
|
+
- float/int: y coordinate for drawing
|
|
61
|
+
- bool: Whether the symbol needs to be drawn
|
|
62
|
+
"""
|
|
39
63
|
|
|
40
64
|
font_size = (
|
|
41
65
|
checkbox_radio_font_size(widget) if middleware.size is None else middleware.size
|
|
@@ -60,7 +84,16 @@ def check_radio_handler(
|
|
|
60
84
|
def signature_image_handler(
|
|
61
85
|
widget: dict, middleware: Union[Signature, Image], images_to_draw: list
|
|
62
86
|
) -> bool:
|
|
63
|
-
"""
|
|
87
|
+
"""Prepares image data for signature and image widgets.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
widget: PDF form widget dictionary containing Rect coordinates
|
|
91
|
+
middleware: Signature or Image middleware instance
|
|
92
|
+
images_to_draw: List to append image drawing parameters to
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
bool: True if an image needs to be drawn, False otherwise
|
|
96
|
+
"""
|
|
64
97
|
|
|
65
98
|
stream = middleware.stream
|
|
66
99
|
any_image_to_draw = False
|
|
@@ -86,7 +119,19 @@ def signature_image_handler(
|
|
|
86
119
|
def text_handler(
|
|
87
120
|
widget: dict, middleware: Text
|
|
88
121
|
) -> Tuple[Text, Union[float, int], Union[float, int], bool]:
|
|
89
|
-
"""
|
|
122
|
+
"""Prepares text field drawing parameters.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
widget: PDF form widget dictionary containing Rect and properties
|
|
126
|
+
middleware: Text middleware instance with text properties
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Tuple containing:
|
|
130
|
+
- Text: The text middleware to draw
|
|
131
|
+
- float/int: x coordinate for drawing
|
|
132
|
+
- float/int: y coordinate for drawing
|
|
133
|
+
- bool: Always True for text fields (they always need drawing)
|
|
134
|
+
"""
|
|
90
135
|
|
|
91
136
|
middleware.text_line_x_coordinates = get_text_line_x_coordinates(widget, middleware)
|
|
92
137
|
x, y = get_draw_text_coordinates(widget, middleware)
|
|
@@ -103,7 +148,15 @@ def border_handler(
|
|
|
103
148
|
ellipse_borders_to_draw: list,
|
|
104
149
|
line_borders_to_draw: list,
|
|
105
150
|
) -> None:
|
|
106
|
-
"""
|
|
151
|
+
"""Prepares border drawing parameters for widgets.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
widget: PDF form widget dictionary containing Rect coordinates
|
|
155
|
+
middleware: Any widget middleware instance
|
|
156
|
+
rect_borders_to_draw: List to append rectangle border parameters to
|
|
157
|
+
ellipse_borders_to_draw: List to append ellipse border parameters to
|
|
158
|
+
line_borders_to_draw: List to append line border parameters to
|
|
159
|
+
"""
|
|
107
160
|
|
|
108
161
|
if (
|
|
109
162
|
isinstance(middleware, Radio)
|
|
@@ -136,7 +189,16 @@ def border_handler(
|
|
|
136
189
|
|
|
137
190
|
|
|
138
191
|
def get_drawn_stream(to_draw: dict, stream: bytes, action: str) -> bytes:
|
|
139
|
-
"""
|
|
192
|
+
"""Applies drawing operations to a PDF stream.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
to_draw: Dictionary mapping page numbers to drawing parameters
|
|
196
|
+
stream: Input PDF as bytes
|
|
197
|
+
action: Type of drawing operation ('text', 'image', 'rect', etc.)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
bytes: Modified PDF with drawings applied
|
|
201
|
+
"""
|
|
140
202
|
|
|
141
203
|
watermark_list = []
|
|
142
204
|
for page, stuffs in to_draw.items():
|
|
@@ -153,7 +215,20 @@ def fill(
|
|
|
153
215
|
template_stream: bytes,
|
|
154
216
|
widgets: Dict[str, WIDGET_TYPES],
|
|
155
217
|
) -> bytes:
|
|
156
|
-
"""Fills a PDF using
|
|
218
|
+
"""Fills a PDF form using watermark technique for complex rendering.
|
|
219
|
+
|
|
220
|
+
This method:
|
|
221
|
+
- Handles text, images, borders for all widget types
|
|
222
|
+
- Preserves original form fields while adding visual elements
|
|
223
|
+
- Supports complex cases like multiline text and image scaling
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
template_stream: Input PDF form as bytes
|
|
227
|
+
widgets: Dictionary mapping field names to widget middleware
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
bytes: Filled PDF form as bytes
|
|
231
|
+
"""
|
|
157
232
|
|
|
158
233
|
texts_to_draw = {}
|
|
159
234
|
images_to_draw = {}
|
|
@@ -226,7 +301,12 @@ def fill(
|
|
|
226
301
|
|
|
227
302
|
|
|
228
303
|
def enable_adobe_mode(pdf: PdfReader, adobe_mode: bool) -> None:
|
|
229
|
-
"""
|
|
304
|
+
"""Configures PDF for Adobe Acrobat compatibility.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
pdf: PdfReader instance of the PDF
|
|
308
|
+
adobe_mode: If True, sets NeedAppearances flag for Acrobat
|
|
309
|
+
"""
|
|
230
310
|
|
|
231
311
|
if adobe_mode and AcroForm in pdf.trailer[Root]:
|
|
232
312
|
pdf.trailer[Root][AcroForm].update(
|
|
@@ -240,7 +320,22 @@ def simple_fill(
|
|
|
240
320
|
flatten: bool = False,
|
|
241
321
|
adobe_mode: bool = False,
|
|
242
322
|
) -> bytes:
|
|
243
|
-
"""Fills a PDF form
|
|
323
|
+
"""Fills a PDF form by directly modifying form fields.
|
|
324
|
+
|
|
325
|
+
This method:
|
|
326
|
+
- Updates field values directly in the PDF
|
|
327
|
+
- Supports flattening to make fields read-only
|
|
328
|
+
- Works with Adobe Acrobat compatibility mode
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
template: Input PDF form as bytes
|
|
332
|
+
widgets: Dictionary mapping field names to widget middleware
|
|
333
|
+
flatten: If True, makes form fields read-only
|
|
334
|
+
adobe_mode: If True, enables Adobe Acrobat compatibility
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
bytes: Filled PDF form as bytes
|
|
338
|
+
"""
|
|
244
339
|
|
|
245
340
|
pdf = PdfReader(stream_to_io(template))
|
|
246
341
|
enable_adobe_mode(pdf, adobe_mode)
|