PyPDFForm 2.2.4__tar.gz → 2.2.6__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.2.4 → pypdfform-2.2.6}/PKG-INFO +1 -1
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/__init__.py +1 -1
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/constants.py +3 -1
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/coordinate.py +76 -32
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/filler.py +51 -30
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/patterns.py +14 -9
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/utils.py +2 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/watermark.py +91 -96
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/base.py +1 -1
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/signature.py +1 -1
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/wrapper.py +9 -6
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm.egg-info/PKG-INFO +1 -1
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_adobe_mode.py +65 -4
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_dropdown_simple.py +1 -1
- {pypdfform-2.2.4 → pypdfform-2.2.6}/LICENSE +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/adapter.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/font.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/image.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/middleware/__init__.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/middleware/base.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/middleware/checkbox.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/middleware/dropdown.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/middleware/image.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/middleware/radio.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/middleware/signature.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/middleware/text.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/template.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/__init__.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/bedrock.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/checkbox.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/dropdown.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/image.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/radio.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm/widgets/text.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm.egg-info/SOURCES.txt +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm.egg-info/requires.txt +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/README.md +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/pyproject.toml +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/setup.cfg +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_create_widget.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_dropdown.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_fill_max_length_text_field_simple.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_fill_method.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_functional.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_functional_simple.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_paragraph.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_paragraph_simple.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_preview.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_signature.py +0 -0
- {pypdfform-2.2.4 → pypdfform-2.2.6}/tests/test_use_full_widget_name.py +0 -0
|
@@ -32,7 +32,7 @@ VERSION_IDENTIFIERS = [
|
|
|
32
32
|
b"%PDF-1.7",
|
|
33
33
|
b"%PDF-2.0",
|
|
34
34
|
]
|
|
35
|
-
VERSION_IDENTIFIER_PREFIX =
|
|
35
|
+
VERSION_IDENTIFIER_PREFIX = "%PDF-".encode("utf-8")
|
|
36
36
|
|
|
37
37
|
WIDGET_TYPES = Union[Text, Checkbox, Radio, Dropdown, Signature, Image]
|
|
38
38
|
|
|
@@ -50,6 +50,7 @@ Ff = "/Ff"
|
|
|
50
50
|
Tx = "/Tx"
|
|
51
51
|
V = "/V"
|
|
52
52
|
AP = "/AP"
|
|
53
|
+
I = "/I" # noqa: E741
|
|
53
54
|
N = "/N"
|
|
54
55
|
Sig = "/Sig"
|
|
55
56
|
DA = "/DA"
|
|
@@ -75,6 +76,7 @@ Off = "/Off"
|
|
|
75
76
|
# For Adobe Acrobat
|
|
76
77
|
AcroForm = "/AcroForm"
|
|
77
78
|
Root = "/Root"
|
|
79
|
+
Fields = "/Fields"
|
|
78
80
|
NeedAppearances = "/NeedAppearances"
|
|
79
81
|
|
|
80
82
|
# Field flag bits
|
|
@@ -28,25 +28,51 @@ from .utils import extract_widget_property, handle_color, stream_to_io
|
|
|
28
28
|
from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def get_draw_border_coordinates(widget: dict, shape: str) ->
|
|
32
|
-
"""Calculates coordinates for drawing widget borders.
|
|
31
|
+
def get_draw_border_coordinates(widget: dict, shape: str) -> dict:
|
|
32
|
+
"""Calculates coordinates for drawing widget borders in PDF coordinate space.
|
|
33
33
|
|
|
34
34
|
Args:
|
|
35
|
-
widget: PDF form widget dictionary containing Rect coordinates
|
|
36
|
-
shape: Type of border to draw
|
|
35
|
+
widget: PDF form widget dictionary containing Rect coordinates (in PDF points)
|
|
36
|
+
shape: Type of border to draw:
|
|
37
|
+
- "rect": Standard rectangular border
|
|
38
|
+
- "ellipse": Circular/oval border
|
|
39
|
+
- "line": Straight line border
|
|
37
40
|
|
|
38
41
|
Returns:
|
|
39
|
-
|
|
40
|
-
For
|
|
41
|
-
|
|
42
|
+
dict: Coordinate dictionary with different keys depending on shape:
|
|
43
|
+
- For "rect":
|
|
44
|
+
{
|
|
45
|
+
"x": bottom-left x,
|
|
46
|
+
"y": bottom-left y,
|
|
47
|
+
"width": total width,
|
|
48
|
+
"height": total height
|
|
49
|
+
}
|
|
50
|
+
- For "ellipse":
|
|
51
|
+
{
|
|
52
|
+
"x1": left bound,
|
|
53
|
+
"y1": bottom bound,
|
|
54
|
+
"x2": right bound,
|
|
55
|
+
"y2": top bound
|
|
56
|
+
}
|
|
57
|
+
- For "line":
|
|
58
|
+
{
|
|
59
|
+
"src_x": start x,
|
|
60
|
+
"src_y": start y,
|
|
61
|
+
"dest_x": end x,
|
|
62
|
+
"dest_y": end y
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Note:
|
|
66
|
+
All coordinates are in PDF points (1/72 inch) with origin (0,0) at bottom-left.
|
|
67
|
+
For ellipses, the bounds form a square that would contain the ellipse.
|
|
42
68
|
"""
|
|
43
69
|
|
|
44
|
-
result =
|
|
45
|
-
float(widget[Rect][0]),
|
|
46
|
-
float(widget[Rect][1]),
|
|
47
|
-
abs(float(widget[Rect][0]) - float(widget[Rect][2])),
|
|
48
|
-
abs(float(widget[Rect][1]) - float(widget[Rect][3])),
|
|
49
|
-
|
|
70
|
+
result = {
|
|
71
|
+
"x": float(widget[Rect][0]),
|
|
72
|
+
"y": float(widget[Rect][1]),
|
|
73
|
+
"width": abs(float(widget[Rect][0]) - float(widget[Rect][2])),
|
|
74
|
+
"height": abs(float(widget[Rect][1]) - float(widget[Rect][3])),
|
|
75
|
+
}
|
|
50
76
|
|
|
51
77
|
if shape == "ellipse":
|
|
52
78
|
width = abs(float(widget[Rect][0]) - float(widget[Rect][2]))
|
|
@@ -57,19 +83,19 @@ def get_draw_border_coordinates(widget: dict, shape: str) -> List[float]:
|
|
|
57
83
|
|
|
58
84
|
less = min(width, height)
|
|
59
85
|
|
|
60
|
-
result =
|
|
61
|
-
width_mid - less / 2,
|
|
62
|
-
height_mid - less / 2,
|
|
63
|
-
width_mid + less / 2,
|
|
64
|
-
height_mid + less / 2,
|
|
65
|
-
|
|
86
|
+
result = {
|
|
87
|
+
"x1": width_mid - less / 2,
|
|
88
|
+
"y1": height_mid - less / 2,
|
|
89
|
+
"x2": width_mid + less / 2,
|
|
90
|
+
"y2": height_mid + less / 2,
|
|
91
|
+
}
|
|
66
92
|
elif shape == "line":
|
|
67
|
-
result =
|
|
68
|
-
float(widget[Rect][0]),
|
|
69
|
-
float(widget[Rect][1]),
|
|
70
|
-
float(widget[Rect][2]),
|
|
71
|
-
float(widget[Rect][1]),
|
|
72
|
-
|
|
93
|
+
result = {
|
|
94
|
+
"src_x": float(widget[Rect][0]),
|
|
95
|
+
"src_y": float(widget[Rect][1]),
|
|
96
|
+
"dest_x": float(widget[Rect][2]),
|
|
97
|
+
"dest_y": float(widget[Rect][1]),
|
|
98
|
+
}
|
|
73
99
|
|
|
74
100
|
return result
|
|
75
101
|
|
|
@@ -397,14 +423,32 @@ def generate_coordinate_grid(
|
|
|
397
423
|
current = margin
|
|
398
424
|
while current < width:
|
|
399
425
|
lines_by_page[i + 1].append(
|
|
400
|
-
|
|
426
|
+
{
|
|
427
|
+
"src_x": current,
|
|
428
|
+
"src_y": 0,
|
|
429
|
+
"dest_x": current,
|
|
430
|
+
"dest_y": height,
|
|
431
|
+
"border_color": handle_color([r, g, b]),
|
|
432
|
+
"background_color": None,
|
|
433
|
+
"border_width": 1,
|
|
434
|
+
"dash_array": None,
|
|
435
|
+
}
|
|
401
436
|
)
|
|
402
437
|
current += margin
|
|
403
438
|
|
|
404
439
|
current = margin
|
|
405
440
|
while current < height:
|
|
406
441
|
lines_by_page[i + 1].append(
|
|
407
|
-
|
|
442
|
+
{
|
|
443
|
+
"src_x": 0,
|
|
444
|
+
"src_y": current,
|
|
445
|
+
"dest_x": width,
|
|
446
|
+
"dest_y": current,
|
|
447
|
+
"border_color": handle_color([r, g, b]),
|
|
448
|
+
"background_color": None,
|
|
449
|
+
"border_width": 1,
|
|
450
|
+
"dash_array": None,
|
|
451
|
+
}
|
|
408
452
|
)
|
|
409
453
|
current += margin
|
|
410
454
|
|
|
@@ -419,11 +463,11 @@ def generate_coordinate_grid(
|
|
|
419
463
|
text.font_size = font_size
|
|
420
464
|
text.font_color = color
|
|
421
465
|
texts_by_page[i + 1].append(
|
|
422
|
-
|
|
423
|
-
text,
|
|
424
|
-
x - stringWidth(value, DEFAULT_FONT, font_size),
|
|
425
|
-
y - font_size,
|
|
426
|
-
|
|
466
|
+
{
|
|
467
|
+
"widget": text,
|
|
468
|
+
"x": x - stringWidth(value, DEFAULT_FONT, font_size),
|
|
469
|
+
"y": y - font_size,
|
|
470
|
+
}
|
|
427
471
|
)
|
|
428
472
|
y += margin
|
|
429
473
|
x += margin
|
|
@@ -16,10 +16,11 @@ from io import BytesIO
|
|
|
16
16
|
from typing import Dict, Tuple, Union, cast
|
|
17
17
|
|
|
18
18
|
from pypdf import PdfReader, PdfWriter
|
|
19
|
-
from pypdf.generic import BooleanObject, DictionaryObject,
|
|
19
|
+
from pypdf.generic import (ArrayObject, BooleanObject, DictionaryObject,
|
|
20
|
+
IndirectObject, NameObject)
|
|
20
21
|
|
|
21
22
|
from .constants import (BUTTON_STYLES, DEFAULT_RADIO_STYLE, WIDGET_TYPES,
|
|
22
|
-
AcroForm, Annots, NeedAppearances, Root, U)
|
|
23
|
+
AcroForm, Annots, Fields, NeedAppearances, Root, U)
|
|
23
24
|
from .coordinate import (get_draw_border_coordinates,
|
|
24
25
|
get_draw_checkbox_radio_coordinates,
|
|
25
26
|
get_draw_image_coordinates_resolutions,
|
|
@@ -104,13 +105,13 @@ def signature_image_handler(
|
|
|
104
105
|
widget, middleware.preserve_aspect_ratio, image_width, image_height
|
|
105
106
|
)
|
|
106
107
|
images_to_draw.append(
|
|
107
|
-
|
|
108
|
-
stream,
|
|
109
|
-
x,
|
|
110
|
-
y,
|
|
111
|
-
width,
|
|
112
|
-
height,
|
|
113
|
-
|
|
108
|
+
{
|
|
109
|
+
"stream": stream,
|
|
110
|
+
"x": x,
|
|
111
|
+
"y": y,
|
|
112
|
+
"width": width,
|
|
113
|
+
"height": height,
|
|
114
|
+
}
|
|
114
115
|
)
|
|
115
116
|
|
|
116
117
|
return any_image_to_draw
|
|
@@ -172,19 +173,24 @@ def border_handler(
|
|
|
172
173
|
shape = "rect"
|
|
173
174
|
|
|
174
175
|
list_to_append.append(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
middleware.border_color,
|
|
178
|
-
middleware.background_color,
|
|
179
|
-
middleware.border_width,
|
|
180
|
-
middleware.dash_array,
|
|
181
|
-
|
|
176
|
+
{
|
|
177
|
+
**get_draw_border_coordinates(widget, shape),
|
|
178
|
+
"border_color": middleware.border_color,
|
|
179
|
+
"background_color": middleware.background_color,
|
|
180
|
+
"border_width": middleware.border_width,
|
|
181
|
+
"dash_array": middleware.dash_array,
|
|
182
|
+
}
|
|
182
183
|
)
|
|
183
184
|
|
|
184
185
|
if shape == "line":
|
|
185
186
|
rect_borders_to_draw.append(
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
{
|
|
188
|
+
**get_draw_border_coordinates(widget, "rect"),
|
|
189
|
+
"border_color": None,
|
|
190
|
+
"background_color": middleware.background_color,
|
|
191
|
+
"border_width": 0,
|
|
192
|
+
"dash_array": None,
|
|
193
|
+
}
|
|
188
194
|
)
|
|
189
195
|
|
|
190
196
|
|
|
@@ -281,11 +287,11 @@ def fill(
|
|
|
281
287
|
]
|
|
282
288
|
):
|
|
283
289
|
texts_to_draw[page].append(
|
|
284
|
-
|
|
285
|
-
to_draw,
|
|
286
|
-
x,
|
|
287
|
-
y,
|
|
288
|
-
|
|
290
|
+
{
|
|
291
|
+
"widget": to_draw,
|
|
292
|
+
"x": x,
|
|
293
|
+
"y": y,
|
|
294
|
+
}
|
|
289
295
|
)
|
|
290
296
|
|
|
291
297
|
result = template_stream
|
|
@@ -300,19 +306,34 @@ def fill(
|
|
|
300
306
|
return result
|
|
301
307
|
|
|
302
308
|
|
|
303
|
-
def enable_adobe_mode(
|
|
304
|
-
"""Configures PDF for Adobe Acrobat compatibility
|
|
309
|
+
def enable_adobe_mode(reader: PdfReader, writer: PdfWriter, adobe_mode: bool) -> None:
|
|
310
|
+
"""Configures the PDF for Adobe Acrobat compatibility by setting the NeedAppearances flag
|
|
311
|
+
and ensuring the AcroForm structure is properly initialized.
|
|
305
312
|
|
|
306
313
|
Args:
|
|
307
|
-
|
|
308
|
-
|
|
314
|
+
reader: PdfReader instance of the PDF
|
|
315
|
+
writer: PdfWriter instance to configure
|
|
316
|
+
adobe_mode: If True, enables Adobe Acrobat compatibility mode
|
|
309
317
|
"""
|
|
310
318
|
|
|
311
|
-
if adobe_mode
|
|
312
|
-
|
|
319
|
+
if not adobe_mode:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
# https://stackoverflow.com/questions/47288578/pdf-form-filled-with-pypdf2-does-not-show-in-print
|
|
323
|
+
if AcroForm in reader.trailer[Root]:
|
|
324
|
+
reader.trailer[Root][AcroForm].update(
|
|
313
325
|
{NameObject(NeedAppearances): BooleanObject(True)}
|
|
314
326
|
)
|
|
315
327
|
|
|
328
|
+
if AcroForm not in writer.root_object:
|
|
329
|
+
writer.root_object.update(
|
|
330
|
+
{NameObject(AcroForm): IndirectObject(len(writer.root_object), 0, writer)}
|
|
331
|
+
)
|
|
332
|
+
writer.root_object[AcroForm][NameObject(NeedAppearances)] = BooleanObject( # noqa
|
|
333
|
+
True
|
|
334
|
+
)
|
|
335
|
+
writer.root_object[AcroForm][NameObject(Fields)] = ArrayObject() # noqa
|
|
336
|
+
|
|
316
337
|
|
|
317
338
|
def simple_fill(
|
|
318
339
|
template: bytes,
|
|
@@ -338,8 +359,8 @@ def simple_fill(
|
|
|
338
359
|
"""
|
|
339
360
|
|
|
340
361
|
pdf = PdfReader(stream_to_io(template))
|
|
341
|
-
enable_adobe_mode(pdf, adobe_mode)
|
|
342
362
|
out = PdfWriter()
|
|
363
|
+
enable_adobe_mode(pdf, out, adobe_mode)
|
|
343
364
|
out.append(pdf)
|
|
344
365
|
|
|
345
366
|
radio_button_tracker = {}
|
|
@@ -15,13 +15,13 @@ The module also contains utility functions for common PDF form operations
|
|
|
15
15
|
like updating field values and flattening form fields.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
from pypdf.generic import (DictionaryObject, NameObject,
|
|
19
|
-
TextStringObject)
|
|
18
|
+
from pypdf.generic import (ArrayObject, DictionaryObject, NameObject,
|
|
19
|
+
NumberObject, TextStringObject)
|
|
20
20
|
|
|
21
21
|
from .constants import (AP, AS, BC, BG, BS, CA, DA, DV, FT,
|
|
22
22
|
IMAGE_FIELD_IDENTIFIER, JS, MK, MULTILINE, READ_ONLY,
|
|
23
|
-
TU, A, Btn, Ch, D, Ff, N, Off, Opt, Parent, Q, S,
|
|
24
|
-
T, Tx, V, W, Yes)
|
|
23
|
+
TU, A, Btn, Ch, D, Ff, I, N, Off, Opt, Parent, Q, S,
|
|
24
|
+
Sig, T, Tx, V, W, Yes)
|
|
25
25
|
from .middleware.checkbox import Checkbox
|
|
26
26
|
from .middleware.dropdown import Dropdown
|
|
27
27
|
from .middleware.image import Image
|
|
@@ -160,13 +160,17 @@ def simple_update_radio_value(annot: DictionaryObject) -> None:
|
|
|
160
160
|
"""Update radio button annotation values to selected state.
|
|
161
161
|
|
|
162
162
|
Modifies the appearance state (AS) of a radio button annotation and updates
|
|
163
|
-
the parent's value (V) to reflect the selected state.
|
|
164
|
-
|
|
163
|
+
the parent's value (V) to reflect the selected state. Removes 'Opt' entry
|
|
164
|
+
from parent dictionary if present. Uses the annotation's appearance
|
|
165
|
+
dictionary (AP/N) to determine valid states.
|
|
165
166
|
|
|
166
167
|
Args:
|
|
167
168
|
annot: PDF radio button annotation dictionary to modify
|
|
168
169
|
"""
|
|
169
170
|
|
|
171
|
+
if Opt in annot[Parent]:
|
|
172
|
+
del annot[Parent][Opt] # noqa
|
|
173
|
+
|
|
170
174
|
for each in annot[AP][N]: # noqa
|
|
171
175
|
if str(each) != Off:
|
|
172
176
|
annot[NameObject(AS)] = NameObject(each)
|
|
@@ -177,9 +181,9 @@ def simple_update_radio_value(annot: DictionaryObject) -> None:
|
|
|
177
181
|
def simple_update_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
|
|
178
182
|
"""Update dropdown annotation values based on widget selection.
|
|
179
183
|
|
|
180
|
-
Modifies the value (V)
|
|
181
|
-
reflect the currently selected choice from the widget.
|
|
182
|
-
standalone dropdowns and those with parent annotations.
|
|
184
|
+
Modifies the value (V), appearance (AP), and index (I) of a dropdown
|
|
185
|
+
annotation to reflect the currently selected choice from the widget.
|
|
186
|
+
Handles both standalone dropdowns and those with parent annotations.
|
|
183
187
|
|
|
184
188
|
Args:
|
|
185
189
|
annot: PDF dropdown annotation dictionary to modify
|
|
@@ -194,6 +198,7 @@ def simple_update_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> N
|
|
|
194
198
|
else:
|
|
195
199
|
annot[NameObject(V)] = TextStringObject(widget.choices[widget.value])
|
|
196
200
|
annot[NameObject(AP)] = TextStringObject(widget.choices[widget.value])
|
|
201
|
+
annot[NameObject(I)] = ArrayObject([NumberObject(widget.value)])
|
|
197
202
|
|
|
198
203
|
|
|
199
204
|
def simple_update_text_value(annot: DictionaryObject, widget: Text) -> None:
|
|
@@ -11,6 +11,7 @@ This module contains general-purpose utilities used throughout PyPDFForm:
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from collections.abc import Callable
|
|
14
|
+
from functools import lru_cache
|
|
14
15
|
from io import BytesIO
|
|
15
16
|
from secrets import choice
|
|
16
17
|
from string import ascii_letters, digits, punctuation
|
|
@@ -29,6 +30,7 @@ from .middleware.radio import Radio
|
|
|
29
30
|
from .middleware.text import Text
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
@lru_cache
|
|
32
34
|
def stream_to_io(stream: bytes) -> BinaryIO:
|
|
33
35
|
"""Converts a byte stream to a seekable binary IO object.
|
|
34
36
|
|
|
@@ -22,7 +22,7 @@ from .patterns import WIDGET_KEY_PATTERNS
|
|
|
22
22
|
from .utils import extract_widget_property, stream_to_io
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def draw_text(
|
|
25
|
+
def draw_text(canvas: Canvas, **kwargs) -> None:
|
|
26
26
|
"""Draws text onto a watermark canvas with proper formatting.
|
|
27
27
|
|
|
28
28
|
Handles:
|
|
@@ -32,16 +32,16 @@ def draw_text(*args) -> None:
|
|
|
32
32
|
- Text alignment
|
|
33
33
|
|
|
34
34
|
Args:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
canvas: Canvas object to draw on
|
|
36
|
+
**kwargs: Additional arguments including:
|
|
37
|
+
widget: Text widget with content and properties
|
|
38
|
+
x: X coordinate for drawing
|
|
39
|
+
y: Y coordinate for drawing
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
coordinate_y = args[3]
|
|
42
|
+
widget = kwargs["widget"]
|
|
43
|
+
coordinate_x = kwargs["x"]
|
|
44
|
+
coordinate_y = kwargs["y"]
|
|
45
45
|
|
|
46
46
|
text_to_draw = widget.value
|
|
47
47
|
|
|
@@ -96,106 +96,105 @@ def draw_text(*args) -> None:
|
|
|
96
96
|
canvas.restoreState()
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def draw_rect(
|
|
99
|
+
def draw_rect(canvas: Canvas, **kwargs) -> None:
|
|
100
100
|
"""Draws a rectangle onto a watermark canvas.
|
|
101
101
|
|
|
102
102
|
Args:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
canvas: Canvas object to draw on
|
|
104
|
+
**kwargs: Additional arguments including:
|
|
105
|
+
x: X coordinate of bottom-left corner
|
|
106
|
+
y: Y coordinate of bottom-left corner
|
|
107
|
+
width: Width of rectangle
|
|
108
|
+
height: Height of rectangle
|
|
109
|
+
border_color: Border color
|
|
110
|
+
background_color: Background color
|
|
111
|
+
border_width: Border width
|
|
112
|
+
dash_array: Dash pattern for border
|
|
112
113
|
"""
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
height = args[4]
|
|
115
|
+
x = kwargs["x"]
|
|
116
|
+
y = kwargs["y"]
|
|
117
|
+
width = kwargs["width"]
|
|
118
|
+
height = kwargs["height"]
|
|
119
119
|
|
|
120
120
|
canvas.saveState()
|
|
121
|
-
stroke, fill = set_border_and_background_styles(
|
|
121
|
+
stroke, fill = set_border_and_background_styles(canvas, **kwargs)
|
|
122
122
|
canvas.rect(x, y, width, height, stroke=stroke, fill=fill)
|
|
123
123
|
canvas.restoreState()
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
def draw_ellipse(
|
|
126
|
+
def draw_ellipse(canvas: Canvas, **kwargs) -> None:
|
|
127
127
|
"""Draws an ellipse onto a watermark canvas.
|
|
128
128
|
|
|
129
129
|
Args:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
canvas: Canvas object to draw on
|
|
131
|
+
**kwargs: Additional arguments including:
|
|
132
|
+
x1: X coordinate of first bounding point
|
|
133
|
+
y1: Y coordinate of first bounding point
|
|
134
|
+
x2: X coordinate of second bounding point
|
|
135
|
+
y2: Y coordinate of second bounding point
|
|
136
|
+
border_color: Border color
|
|
137
|
+
background_color: Background color
|
|
138
|
+
border_width: Border width
|
|
139
|
+
dash_array: Dash pattern for border
|
|
139
140
|
"""
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
y2 = args[4]
|
|
142
|
+
x1 = kwargs["x1"]
|
|
143
|
+
y1 = kwargs["y1"]
|
|
144
|
+
x2 = kwargs["x2"]
|
|
145
|
+
y2 = kwargs["y2"]
|
|
146
146
|
|
|
147
147
|
canvas.saveState()
|
|
148
|
-
stroke, fill = set_border_and_background_styles(
|
|
148
|
+
stroke, fill = set_border_and_background_styles(canvas, **kwargs)
|
|
149
149
|
canvas.ellipse(x1, y1, x2, y2, stroke=stroke, fill=fill)
|
|
150
150
|
canvas.restoreState()
|
|
151
151
|
|
|
152
152
|
|
|
153
|
-
def draw_line(
|
|
153
|
+
def draw_line(canvas: Canvas, **kwargs) -> None:
|
|
154
154
|
"""Draws a line onto a watermark canvas.
|
|
155
155
|
|
|
156
156
|
Args:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
canvas: Canvas object to draw on
|
|
158
|
+
**kwargs: Additional arguments including:
|
|
159
|
+
src_x: X coordinate of start point
|
|
160
|
+
src_y: Y coordinate of start point
|
|
161
|
+
dest_x: X coordinate of end point
|
|
162
|
+
dest_y: Y coordinate of end point
|
|
163
|
+
border_color: Line color
|
|
164
|
+
border_width: Line width
|
|
165
|
+
dash_array: Dash pattern for line
|
|
166
166
|
"""
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
dest_y = args[4]
|
|
168
|
+
src_x = kwargs["src_x"]
|
|
169
|
+
src_y = kwargs["src_y"]
|
|
170
|
+
dest_x = kwargs["dest_x"]
|
|
171
|
+
dest_y = kwargs["dest_y"]
|
|
173
172
|
|
|
174
173
|
canvas.saveState()
|
|
175
|
-
set_border_and_background_styles(
|
|
174
|
+
set_border_and_background_styles(canvas, **kwargs)
|
|
176
175
|
canvas.line(src_x, src_y, dest_x, dest_y)
|
|
177
176
|
canvas.restoreState()
|
|
178
177
|
|
|
179
178
|
|
|
180
|
-
def set_border_and_background_styles(
|
|
179
|
+
def set_border_and_background_styles(canvas: Canvas, **kwargs) -> tuple:
|
|
181
180
|
"""Configures stroke and fill styles for drawing operations.
|
|
182
181
|
|
|
183
182
|
Args:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
canvas: Canvas object to configure
|
|
184
|
+
**kwargs: Additional arguments including:
|
|
185
|
+
border_color: Border color
|
|
186
|
+
background_color: Background color
|
|
187
|
+
border_width: Border width
|
|
188
|
+
dash_array: Dash pattern for border
|
|
189
189
|
|
|
190
190
|
Returns:
|
|
191
191
|
tuple: (stroke_flag, fill_flag) indicating which styles were set
|
|
192
192
|
"""
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
dash_array = args[8]
|
|
194
|
+
border_color = kwargs["border_color"]
|
|
195
|
+
background_color = kwargs["background_color"]
|
|
196
|
+
border_width = kwargs["border_width"]
|
|
197
|
+
dash_array = kwargs["dash_array"]
|
|
199
198
|
|
|
200
199
|
stroke = 0
|
|
201
200
|
fill = 0
|
|
@@ -213,24 +212,24 @@ def set_border_and_background_styles(*args) -> tuple:
|
|
|
213
212
|
return stroke, fill
|
|
214
213
|
|
|
215
214
|
|
|
216
|
-
def draw_image(
|
|
215
|
+
def draw_image(canvas: Canvas, **kwargs) -> None:
|
|
217
216
|
"""Draws an image onto a watermark canvas.
|
|
218
217
|
|
|
219
218
|
Args:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
219
|
+
canvas: Canvas object to draw on
|
|
220
|
+
**kwargs: Additional arguments including:
|
|
221
|
+
stream: Image data as bytes
|
|
222
|
+
x: X coordinate for drawing
|
|
223
|
+
y: Y coordinate for drawing
|
|
224
|
+
width: Width of drawn image
|
|
225
|
+
height: Height of drawn image
|
|
226
226
|
"""
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
height = args[5]
|
|
228
|
+
image_stream = kwargs["stream"]
|
|
229
|
+
coordinate_x = kwargs["x"]
|
|
230
|
+
coordinate_y = kwargs["y"]
|
|
231
|
+
width = kwargs["width"]
|
|
232
|
+
height = kwargs["height"]
|
|
234
233
|
|
|
235
234
|
image_buff = BytesIO()
|
|
236
235
|
image_buff.write(image_stream)
|
|
@@ -252,7 +251,7 @@ def create_watermarks_and_draw(
|
|
|
252
251
|
pdf: bytes,
|
|
253
252
|
page_number: int,
|
|
254
253
|
action_type: str,
|
|
255
|
-
actions: List[
|
|
254
|
+
actions: List[dict],
|
|
256
255
|
) -> List[bytes]:
|
|
257
256
|
"""Creates watermarks for each page with specified drawing operations.
|
|
258
257
|
|
|
@@ -277,21 +276,17 @@ def create_watermarks_and_draw(
|
|
|
277
276
|
),
|
|
278
277
|
)
|
|
279
278
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
elif action_type == "rect":
|
|
290
|
-
for each in actions:
|
|
291
|
-
draw_rect(*([canvas, *each]))
|
|
292
|
-
elif action_type == "ellipse":
|
|
279
|
+
action_type_to_func = {
|
|
280
|
+
"image": draw_image,
|
|
281
|
+
"text": draw_text,
|
|
282
|
+
"line": draw_line,
|
|
283
|
+
"rect": draw_rect,
|
|
284
|
+
"ellipse": draw_ellipse,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if action_type_to_func.get(action_type):
|
|
293
288
|
for each in actions:
|
|
294
|
-
|
|
289
|
+
action_type_to_func[action_type](canvas, **each)
|
|
295
290
|
|
|
296
291
|
canvas.save()
|
|
297
292
|
buff.seek(0)
|
|
@@ -83,7 +83,7 @@ class Widget:
|
|
|
83
83
|
)
|
|
84
84
|
self.acro_form_params[param] = value
|
|
85
85
|
elif user_input in self.NONE_DEFAULTS:
|
|
86
|
-
self.acro_form_params[param] = None
|
|
86
|
+
self.acro_form_params[param] = None # noqa
|
|
87
87
|
|
|
88
88
|
for each in self.ALLOWED_NON_ACRO_FORM_PARAMS:
|
|
89
89
|
if each in kwargs:
|
|
@@ -101,7 +101,7 @@ class SignatureWidget:
|
|
|
101
101
|
|
|
102
102
|
input_pdf = PdfReader(stream_to_io(stream))
|
|
103
103
|
page_count = len(input_pdf.pages)
|
|
104
|
-
pdf = PdfReader(stream_to_io(BEDROCK_PDF))
|
|
104
|
+
pdf = PdfReader(stream_to_io(BEDROCK_PDF)) # noqa
|
|
105
105
|
out = PdfWriter()
|
|
106
106
|
out.append(pdf)
|
|
107
107
|
|
|
@@ -620,11 +620,11 @@ class PdfWrapper(FormWrapper):
|
|
|
620
620
|
page_number,
|
|
621
621
|
"text",
|
|
622
622
|
[
|
|
623
|
-
|
|
624
|
-
new_widget,
|
|
625
|
-
x,
|
|
626
|
-
y,
|
|
627
|
-
|
|
623
|
+
{
|
|
624
|
+
"widget": new_widget,
|
|
625
|
+
"x": x,
|
|
626
|
+
"y": y,
|
|
627
|
+
}
|
|
628
628
|
],
|
|
629
629
|
)
|
|
630
630
|
|
|
@@ -667,7 +667,10 @@ class PdfWrapper(FormWrapper):
|
|
|
667
667
|
image = fp_or_f_obj_or_stream_to_stream(image)
|
|
668
668
|
image = rotate_image(image, rotation)
|
|
669
669
|
watermarks = create_watermarks_and_draw(
|
|
670
|
-
self.stream,
|
|
670
|
+
self.stream,
|
|
671
|
+
page_number,
|
|
672
|
+
"image",
|
|
673
|
+
[{"stream": image, "x": x, "y": y, "width": width, "height": height}],
|
|
671
674
|
)
|
|
672
675
|
|
|
673
676
|
stream_with_widgets = self.read()
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
-
from PyPDFForm import FormWrapper
|
|
5
|
+
from PyPDFForm import FormWrapper, PdfWrapper
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def
|
|
8
|
+
def test_dropdown_two(sample_template_with_dropdown, pdf_samples, request):
|
|
9
9
|
expected_path = os.path.join(
|
|
10
|
-
pdf_samples, "adobe_mode", "dropdown", "
|
|
10
|
+
pdf_samples, "adobe_mode", "dropdown", "dropdown_two.pdf"
|
|
11
11
|
)
|
|
12
12
|
with open(expected_path, "rb+") as f:
|
|
13
13
|
obj = FormWrapper(sample_template_with_dropdown).fill(
|
|
@@ -19,7 +19,7 @@ def test_dropdown_one(sample_template_with_dropdown, pdf_samples, request):
|
|
|
19
19
|
"check_2": True,
|
|
20
20
|
"check_3": True,
|
|
21
21
|
"radio_1": 1,
|
|
22
|
-
"dropdown_1":
|
|
22
|
+
"dropdown_1": 1,
|
|
23
23
|
},
|
|
24
24
|
adobe_mode=True,
|
|
25
25
|
)
|
|
@@ -95,3 +95,64 @@ def test_issue_613(pdf_samples, request):
|
|
|
95
95
|
|
|
96
96
|
assert len(obj.read()) == len(expected)
|
|
97
97
|
assert obj.stream == expected
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_sample_template_libary(pdf_samples, request):
|
|
101
|
+
expected_path = os.path.join(
|
|
102
|
+
pdf_samples, "adobe_mode", "test_sample_template_libary.pdf"
|
|
103
|
+
)
|
|
104
|
+
template = (
|
|
105
|
+
PdfWrapper(os.path.join(pdf_samples, "dummy.pdf"))
|
|
106
|
+
.create_widget(
|
|
107
|
+
widget_type="text",
|
|
108
|
+
name="new_text_field_widget",
|
|
109
|
+
page_number=1,
|
|
110
|
+
x=60,
|
|
111
|
+
y=710,
|
|
112
|
+
)
|
|
113
|
+
.create_widget(
|
|
114
|
+
widget_type="checkbox",
|
|
115
|
+
name="new_checkbox_widget",
|
|
116
|
+
page_number=1,
|
|
117
|
+
x=100,
|
|
118
|
+
y=600,
|
|
119
|
+
)
|
|
120
|
+
.create_widget(
|
|
121
|
+
widget_type="radio",
|
|
122
|
+
name="new_radio_group",
|
|
123
|
+
page_number=1,
|
|
124
|
+
x=[50, 100, 150],
|
|
125
|
+
y=[50, 100, 150],
|
|
126
|
+
)
|
|
127
|
+
.create_widget(
|
|
128
|
+
widget_type="dropdown",
|
|
129
|
+
name="new_dropdown_widget",
|
|
130
|
+
page_number=1,
|
|
131
|
+
x=300,
|
|
132
|
+
y=710,
|
|
133
|
+
options=[
|
|
134
|
+
"foo",
|
|
135
|
+
"bar",
|
|
136
|
+
"foobar",
|
|
137
|
+
],
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
with open(expected_path, "rb+") as f:
|
|
142
|
+
obj = FormWrapper(template.read()).fill(
|
|
143
|
+
{
|
|
144
|
+
"new_text_field_widget": "test text",
|
|
145
|
+
"new_checkbox_widget": True,
|
|
146
|
+
"new_radio_group": 1,
|
|
147
|
+
"new_dropdown_widget": 2,
|
|
148
|
+
},
|
|
149
|
+
adobe_mode=True,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
request.config.results["expected_path"] = expected_path
|
|
153
|
+
request.config.results["stream"] = obj.read()
|
|
154
|
+
|
|
155
|
+
expected = f.read()
|
|
156
|
+
|
|
157
|
+
assert len(obj.read()) == len(expected)
|
|
158
|
+
assert obj.stream == expected
|
|
@@ -75,7 +75,7 @@ def test_dropdown_two(sample_template_with_dropdown, pdf_samples, request):
|
|
|
75
75
|
|
|
76
76
|
def test_dropdown_two_flatten(sample_template_with_dropdown, pdf_samples, request):
|
|
77
77
|
expected_path = os.path.join(
|
|
78
|
-
pdf_samples, "simple", "dropdown", "
|
|
78
|
+
pdf_samples, "simple", "dropdown", "dropdown_two_flatten.pdf"
|
|
79
79
|
)
|
|
80
80
|
with open(expected_path, "rb+") as f:
|
|
81
81
|
obj = FormWrapper(sample_template_with_dropdown).fill(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|