PyPDFForm 3.5.1__py3-none-any.whl → 4.2.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.
- PyPDFForm/__init__.py +5 -3
- PyPDFForm/adapter.py +33 -1
- PyPDFForm/ap.py +99 -0
- PyPDFForm/assets/__init__.py +0 -0
- PyPDFForm/assets/blank.py +100 -0
- PyPDFForm/constants.py +20 -2
- PyPDFForm/coordinate.py +7 -11
- PyPDFForm/deprecation.py +30 -0
- PyPDFForm/filler.py +17 -36
- PyPDFForm/font.py +16 -16
- PyPDFForm/hooks.py +169 -31
- PyPDFForm/image.py +0 -3
- PyPDFForm/middleware/__init__.py +35 -0
- PyPDFForm/middleware/base.py +24 -5
- PyPDFForm/middleware/checkbox.py +18 -1
- PyPDFForm/middleware/signature.py +0 -1
- PyPDFForm/patterns.py +71 -13
- PyPDFForm/raw/__init__.py +37 -0
- PyPDFForm/raw/circle.py +65 -0
- PyPDFForm/raw/ellipse.py +69 -0
- PyPDFForm/raw/image.py +79 -0
- PyPDFForm/raw/line.py +65 -0
- PyPDFForm/raw/rect.py +70 -0
- PyPDFForm/raw/text.py +73 -0
- PyPDFForm/template.py +114 -10
- PyPDFForm/types.py +49 -0
- PyPDFForm/utils.py +31 -41
- PyPDFForm/watermark.py +153 -44
- PyPDFForm/widgets/__init__.py +1 -0
- PyPDFForm/widgets/base.py +79 -59
- PyPDFForm/widgets/checkbox.py +30 -30
- PyPDFForm/widgets/dropdown.py +42 -40
- PyPDFForm/widgets/image.py +17 -16
- PyPDFForm/widgets/radio.py +27 -28
- PyPDFForm/widgets/signature.py +96 -60
- PyPDFForm/widgets/text.py +40 -40
- PyPDFForm/wrapper.py +256 -240
- {pypdfform-3.5.1.dist-info → pypdfform-4.2.0.dist-info}/METADATA +33 -26
- pypdfform-4.2.0.dist-info/RECORD +47 -0
- {pypdfform-3.5.1.dist-info → pypdfform-4.2.0.dist-info}/licenses/LICENSE +1 -1
- pypdfform-3.5.1.dist-info/RECORD +0 -35
- /PyPDFForm/{widgets → assets}/bedrock.py +0 -0
- {pypdfform-3.5.1.dist-info → pypdfform-4.2.0.dist-info}/WHEEL +0 -0
- {pypdfform-3.5.1.dist-info → pypdfform-4.2.0.dist-info}/top_level.txt +0 -0
PyPDFForm/hooks.py
CHANGED
|
@@ -8,22 +8,21 @@ of checkbox and radio button widgets. It also provides functions for flattening
|
|
|
8
8
|
generic and radio button widgets. These hooks are triggered during the PDF form
|
|
9
9
|
filling process, allowing for customization of the form's appearance and behavior.
|
|
10
10
|
"""
|
|
11
|
-
# TODO: In `trigger_widget_hooks`, the PDF is read and written in each call. If this function is part of a larger workflow, consider passing `PdfReader` and `PdfWriter` objects to avoid redundant parsing and writing, allowing modifications to be accumulated and written once.
|
|
12
|
-
# TODO: String manipulations (split/join) in `update_text_field_font`, `update_text_field_font_size`, and `update_text_field_font_color` could be optimized for very long `DA` strings, potentially using more efficient string manipulation techniques or regex if the structure is consistent.
|
|
13
|
-
# TODO: The `get_widget_key` function is called in a loop within `trigger_widget_hooks`. If its internal logic is complex, consider caching its results or optimizing its implementation to avoid redundant computations.
|
|
14
|
-
# TODO: In `flatten_radio` and `flatten_generic`, `annot.get(NameObject(Ff), 0)` is called twice within the conditional. Store this value in a local variable to avoid redundant dictionary lookups.
|
|
15
11
|
|
|
16
12
|
import sys
|
|
17
13
|
from io import BytesIO
|
|
18
|
-
from typing import cast
|
|
14
|
+
from typing import TextIO, Union, cast
|
|
19
15
|
|
|
20
16
|
from pypdf import PdfReader, PdfWriter
|
|
21
17
|
from pypdf.generic import (ArrayObject, DictionaryObject, FloatObject,
|
|
22
18
|
NameObject, NumberObject, TextStringObject)
|
|
23
19
|
|
|
24
|
-
from .
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
from .adapter import fp_or_f_obj_or_f_content_to_content
|
|
21
|
+
from .constants import (AA, COMB, DA, FONT_COLOR_IDENTIFIER,
|
|
22
|
+
FONT_SIZE_IDENTIFIER, JS, MULTILINE, READ_ONLY,
|
|
23
|
+
REQUIRED, TU, Action, Annots, Bl, D, E, Ff, Fo,
|
|
24
|
+
JavaScript, MaxLen, Opt, Parent, Q, Rect, S, Type, U,
|
|
25
|
+
X)
|
|
27
26
|
from .template import get_widget_key
|
|
28
27
|
from .utils import stream_to_io
|
|
29
28
|
|
|
@@ -83,19 +82,31 @@ def update_text_field_font(annot: DictionaryObject, val: str) -> None:
|
|
|
83
82
|
Updates the font of a text field annotation.
|
|
84
83
|
|
|
85
84
|
This function modifies the appearance string (DA) in the annotation dictionary
|
|
86
|
-
to change the font used for the text field.
|
|
85
|
+
to change the font used for the text field. It ensures that the provided font
|
|
86
|
+
name is a proper PDF font by checking if it starts with a slash "/".
|
|
87
|
+
The function then correctly identifies and updates the font in the appearance
|
|
88
|
+
stream by locating the existing font identifier (which also starts with a slash).
|
|
87
89
|
|
|
88
90
|
Args:
|
|
89
91
|
annot (DictionaryObject): The annotation dictionary for the text field.
|
|
90
|
-
val (str): The new font name to use for the text field.
|
|
92
|
+
val (str): The new font name to use for the text field. Must start with "/".
|
|
91
93
|
"""
|
|
94
|
+
if not val.startswith("/"):
|
|
95
|
+
return
|
|
92
96
|
if Parent in annot and DA not in annot:
|
|
93
97
|
text_appearance = annot[Parent][DA]
|
|
94
98
|
else:
|
|
95
99
|
text_appearance = annot[DA]
|
|
96
100
|
|
|
97
101
|
text_appearance = text_appearance.split(" ")
|
|
98
|
-
|
|
102
|
+
|
|
103
|
+
index_to_update = 0
|
|
104
|
+
for i, each in enumerate(text_appearance):
|
|
105
|
+
if each.startswith("/"):
|
|
106
|
+
index_to_update = i
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
text_appearance[index_to_update] = val
|
|
99
110
|
new_text_appearance = " ".join(text_appearance)
|
|
100
111
|
|
|
101
112
|
if Parent in annot and DA not in annot:
|
|
@@ -204,7 +215,8 @@ def update_text_field_multiline(annot: DictionaryObject, val: bool) -> None:
|
|
|
204
215
|
val (bool): True to enable multiline, False to disable.
|
|
205
216
|
"""
|
|
206
217
|
if val:
|
|
207
|
-
|
|
218
|
+
# Ff in annot[Parent] only in hooks.py, or when editing instead of retrieving
|
|
219
|
+
if Parent in annot and Ff in annot[Parent]:
|
|
208
220
|
annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
|
|
209
221
|
int(
|
|
210
222
|
annot[NameObject(Parent)][NameObject(Ff)]
|
|
@@ -232,7 +244,7 @@ def update_text_field_comb(annot: DictionaryObject, val: bool) -> None:
|
|
|
232
244
|
val (bool): True to enable comb, False to disable.
|
|
233
245
|
"""
|
|
234
246
|
if val:
|
|
235
|
-
if Parent in annot and Ff
|
|
247
|
+
if Parent in annot and Ff in annot[Parent]:
|
|
236
248
|
annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
|
|
237
249
|
int(
|
|
238
250
|
annot[NameObject(Parent)][NameObject(Ff)]
|
|
@@ -352,12 +364,12 @@ def flatten_generic(annot: DictionaryObject, val: bool) -> None:
|
|
|
352
364
|
annot (DictionaryObject): The annotation dictionary.
|
|
353
365
|
val (bool): True to flatten (make read-only), False to unflatten (make editable).
|
|
354
366
|
"""
|
|
355
|
-
if Parent in annot and Ff not in annot:
|
|
367
|
+
if Parent in annot and (Ff in annot[Parent] or Ff not in annot):
|
|
356
368
|
annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
|
|
357
369
|
(
|
|
358
|
-
int(annot.get(NameObject(Ff), 0)) | READ_ONLY
|
|
370
|
+
int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) | READ_ONLY
|
|
359
371
|
if val
|
|
360
|
-
else int(annot.get(NameObject(Ff), 0)) & ~READ_ONLY
|
|
372
|
+
else int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) & ~READ_ONLY
|
|
361
373
|
)
|
|
362
374
|
)
|
|
363
375
|
else:
|
|
@@ -397,20 +409,146 @@ def update_field_required(annot: DictionaryObject, val: bool) -> None:
|
|
|
397
409
|
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
398
410
|
val (bool): True to set the field as required, False to make it optional.
|
|
399
411
|
"""
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
# )
|
|
408
|
-
# )
|
|
409
|
-
# else:
|
|
410
|
-
annot[NameObject(Ff)] = NumberObject(
|
|
411
|
-
(
|
|
412
|
-
int(annot.get(NameObject(Ff), 0)) | REQUIRED
|
|
413
|
-
if val
|
|
414
|
-
else int(annot.get(NameObject(Ff), 0)) & ~REQUIRED
|
|
412
|
+
if Parent in annot and Ff in annot[Parent]:
|
|
413
|
+
annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
|
|
414
|
+
(
|
|
415
|
+
int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) | REQUIRED
|
|
416
|
+
if val
|
|
417
|
+
else int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) & ~REQUIRED
|
|
418
|
+
)
|
|
415
419
|
)
|
|
420
|
+
else:
|
|
421
|
+
annot[NameObject(Ff)] = NumberObject(
|
|
422
|
+
(
|
|
423
|
+
int(annot.get(NameObject(Ff), 0)) | REQUIRED
|
|
424
|
+
if val
|
|
425
|
+
else int(annot.get(NameObject(Ff), 0)) & ~REQUIRED
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _update_field_javascript(
|
|
431
|
+
annot: DictionaryObject, trigger_event: str, val: Union[str, TextIO]
|
|
432
|
+
) -> None:
|
|
433
|
+
"""
|
|
434
|
+
Updates a specific JavaScript action for a form field annotation.
|
|
435
|
+
|
|
436
|
+
This internal helper function adds or updates a JavaScript action in the
|
|
437
|
+
annotation's additional actions dictionary (AA) for a specific trigger event.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
441
|
+
trigger_event (str): The event that triggers the JavaScript action
|
|
442
|
+
(e.g., E for enter, X for exit, D for down, U for up, Fo for focus, Bl for blur).
|
|
443
|
+
val (Union[str, TextIO]): The JavaScript code to execute. Can be a string
|
|
444
|
+
containing the code or a file-like object/path to a file containing the code.
|
|
445
|
+
"""
|
|
446
|
+
if AA not in annot:
|
|
447
|
+
annot[NameObject(AA)] = DictionaryObject()
|
|
448
|
+
|
|
449
|
+
annot[NameObject(AA)][NameObject(trigger_event)] = DictionaryObject()
|
|
450
|
+
annot[NameObject(AA)][NameObject(trigger_event)][NameObject(Type)] = NameObject(
|
|
451
|
+
Action
|
|
452
|
+
)
|
|
453
|
+
annot[NameObject(AA)][NameObject(trigger_event)][NameObject(S)] = NameObject(
|
|
454
|
+
JavaScript
|
|
416
455
|
)
|
|
456
|
+
annot[NameObject(AA)][NameObject(trigger_event)][NameObject(JS)] = TextStringObject(
|
|
457
|
+
fp_or_f_obj_or_f_content_to_content(val)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def update_field_on_hovered_over_javascript(
|
|
462
|
+
annot: DictionaryObject, val: Union[str, TextIO]
|
|
463
|
+
) -> None:
|
|
464
|
+
"""
|
|
465
|
+
Updates the JavaScript action triggered when the mouse enters the field area.
|
|
466
|
+
|
|
467
|
+
This function sets the 'E' (Enter) entry in the annotation's additional actions
|
|
468
|
+
dictionary, specifying JavaScript code to execute when the cursor enters the field.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
472
|
+
val (Union[str, TextIO]): The JavaScript code to execute.
|
|
473
|
+
"""
|
|
474
|
+
_update_field_javascript(annot, E, val)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def update_field_on_hovered_off_javascript(
|
|
478
|
+
annot: DictionaryObject, val: Union[str, TextIO]
|
|
479
|
+
) -> None:
|
|
480
|
+
"""
|
|
481
|
+
Updates the JavaScript action triggered when the mouse exits the field area.
|
|
482
|
+
|
|
483
|
+
This function sets the 'X' (Exit) entry in the annotation's additional actions
|
|
484
|
+
dictionary, specifying JavaScript code to execute when the cursor leaves the field.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
488
|
+
val (Union[str, TextIO]): The JavaScript code to execute.
|
|
489
|
+
"""
|
|
490
|
+
_update_field_javascript(annot, X, val)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def update_field_on_mouse_pressed_javascript(
|
|
494
|
+
annot: DictionaryObject, val: Union[str, TextIO]
|
|
495
|
+
) -> None:
|
|
496
|
+
"""
|
|
497
|
+
Updates the JavaScript action triggered when the mouse button is pressed down inside the field.
|
|
498
|
+
|
|
499
|
+
This function sets the 'D' (Down) entry in the annotation's additional actions
|
|
500
|
+
dictionary, specifying JavaScript code to execute when the mouse button is pressed.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
504
|
+
val (Union[str, TextIO]): The JavaScript code to execute.
|
|
505
|
+
"""
|
|
506
|
+
_update_field_javascript(annot, D, val)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def update_field_on_mouse_released_javascript(
|
|
510
|
+
annot: DictionaryObject, val: Union[str, TextIO]
|
|
511
|
+
) -> None:
|
|
512
|
+
"""
|
|
513
|
+
Updates the JavaScript action triggered when the mouse button is released inside the field.
|
|
514
|
+
|
|
515
|
+
This function sets the 'U' (Up) entry in the annotation's additional actions
|
|
516
|
+
dictionary, specifying JavaScript code to execute when the mouse button is released.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
520
|
+
val (Union[str, TextIO]): The JavaScript code to execute.
|
|
521
|
+
"""
|
|
522
|
+
_update_field_javascript(annot, U, val)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def update_field_on_focused_javascript(
|
|
526
|
+
annot: DictionaryObject, val: Union[str, TextIO]
|
|
527
|
+
) -> None:
|
|
528
|
+
"""
|
|
529
|
+
Updates the JavaScript action triggered when the field receives input focus.
|
|
530
|
+
|
|
531
|
+
This function sets the 'Fo' (Focus) entry in the annotation's additional actions
|
|
532
|
+
dictionary, specifying JavaScript code to execute when the field gains focus.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
536
|
+
val (Union[str, TextIO]): The JavaScript code to execute.
|
|
537
|
+
"""
|
|
538
|
+
_update_field_javascript(annot, Fo, val)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def update_field_on_blurred_javascript(
|
|
542
|
+
annot: DictionaryObject, val: Union[str, TextIO]
|
|
543
|
+
) -> None:
|
|
544
|
+
"""
|
|
545
|
+
Updates the JavaScript action triggered when the field loses input focus.
|
|
546
|
+
|
|
547
|
+
This function sets the 'Bl' (Blur) entry in the annotation's additional actions
|
|
548
|
+
dictionary, specifying JavaScript code to execute when the field loses focus.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
552
|
+
val (Union[str, TextIO]): The JavaScript code to execute.
|
|
553
|
+
"""
|
|
554
|
+
_update_field_javascript(annot, Bl, val)
|
PyPDFForm/image.py
CHANGED
|
@@ -6,9 +6,6 @@ It includes functions for rotating images, retrieving image dimensions, and
|
|
|
6
6
|
calculating the resolutions for drawing an image on a PDF page, taking into
|
|
7
7
|
account whether to preserve the aspect ratio.
|
|
8
8
|
"""
|
|
9
|
-
# TODO: In `rotate_image` and `get_image_dimensions`, `BytesIO` is used to wrap the image stream. While necessary for PIL, consider if the `image_stream` is already a file-like object in some calling contexts, which could avoid redundant copying to `BytesIO`.
|
|
10
|
-
# TODO: The `rotate_image` function creates a new `BytesIO` object and saves the image to it. For multiple rotations or image manipulations, consider keeping the `PIL.Image.Image` object in memory and performing operations on it directly before a final save to bytes, to avoid repeated I/O operations.
|
|
11
|
-
# TODO: The `get_image_dimensions` function opens the image to get its size. If image dimensions are frequently needed for the same image, consider caching the dimensions to avoid re-opening and re-parsing the image data.
|
|
12
9
|
|
|
13
10
|
from io import BytesIO
|
|
14
11
|
from typing import Tuple, Union
|
PyPDFForm/middleware/__init__.py
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
The `middleware` package provides intermediate classes used internally
|
|
4
|
+
to manage and manipulate PDF form widgets, abstracting away some of the
|
|
5
|
+
low-level PDF details.
|
|
6
|
+
|
|
7
|
+
These classes are typically used during the filling process to represent
|
|
8
|
+
the state and attributes of various form field types within the PDF.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from .checkbox import Checkbox
|
|
14
|
+
from .dropdown import Dropdown
|
|
15
|
+
from .image import Image
|
|
16
|
+
from .radio import Radio
|
|
17
|
+
from .signature import Signature
|
|
18
|
+
from .text import Text
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Widgets:
|
|
23
|
+
"""
|
|
24
|
+
A container class that provides convenient access to all available middleware widget classes.
|
|
25
|
+
|
|
26
|
+
This class acts as a namespace for the various middleware classes defined in the
|
|
27
|
+
`PyPDFForm.middleware` package, making it easier to reference them (e.g., `Widgets.Text`).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
Text = Text
|
|
31
|
+
Checkbox = Checkbox
|
|
32
|
+
Radio = Radio
|
|
33
|
+
Dropdown = Dropdown
|
|
34
|
+
Signature = Signature
|
|
35
|
+
Image = Image
|
PyPDFForm/middleware/base.py
CHANGED
|
@@ -8,7 +8,7 @@ common attributes and methods for all form widgets, such as name, value,
|
|
|
8
8
|
and schema definition.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, List, TextIO, Union
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Widget:
|
|
@@ -25,6 +25,12 @@ class Widget:
|
|
|
25
25
|
"readonly": "flatten_generic",
|
|
26
26
|
"required": "update_field_required",
|
|
27
27
|
"tooltip": "update_field_tooltip",
|
|
28
|
+
"on_hovered_over_javascript": "update_field_on_hovered_over_javascript",
|
|
29
|
+
"on_hovered_off_javascript": "update_field_on_hovered_off_javascript",
|
|
30
|
+
"on_mouse_pressed_javascript": "update_field_on_mouse_pressed_javascript",
|
|
31
|
+
"on_mouse_released_javascript": "update_field_on_mouse_released_javascript",
|
|
32
|
+
"on_focused_javascript": "update_field_on_focused_javascript",
|
|
33
|
+
"on_blurred_javascript": "update_field_on_blurred_javascript",
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
def __init__(
|
|
@@ -42,12 +48,25 @@ class Widget:
|
|
|
42
48
|
super().__init__()
|
|
43
49
|
self._name = name
|
|
44
50
|
self._value = value
|
|
45
|
-
self.
|
|
46
|
-
self.tooltip: str = None # TODO: sync tooltip and desc
|
|
51
|
+
self.tooltip: str = None
|
|
47
52
|
self.readonly: bool = None
|
|
48
53
|
self.required: bool = None
|
|
49
54
|
self.hooks_to_trigger: list = []
|
|
50
55
|
|
|
56
|
+
# coordinate & dimension
|
|
57
|
+
self.x: Union[float, List[float]] = None
|
|
58
|
+
self.y: Union[float, List[float]] = None
|
|
59
|
+
self.width: Union[float, List[float]] = None
|
|
60
|
+
self.height: Union[float, List[float]] = None
|
|
61
|
+
|
|
62
|
+
# javascript
|
|
63
|
+
self.on_hovered_over_javascript: Union[str, TextIO] = None
|
|
64
|
+
self.on_hovered_off_javascript: Union[str, TextIO] = None
|
|
65
|
+
self.on_mouse_pressed_javascript: Union[str, TextIO] = None
|
|
66
|
+
self.on_mouse_released_javascript: Union[str, TextIO] = None
|
|
67
|
+
self.on_focused_javascript: Union[str, TextIO] = None
|
|
68
|
+
self.on_blurred_javascript: Union[str, TextIO] = None
|
|
69
|
+
|
|
51
70
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
52
71
|
"""
|
|
53
72
|
Set an attribute on the widget.
|
|
@@ -107,8 +126,8 @@ class Widget:
|
|
|
107
126
|
"""
|
|
108
127
|
result = {}
|
|
109
128
|
|
|
110
|
-
if self.
|
|
111
|
-
result["description"] = self.
|
|
129
|
+
if self.tooltip is not None:
|
|
130
|
+
result["description"] = self.tooltip
|
|
112
131
|
|
|
113
132
|
return result
|
|
114
133
|
|
PyPDFForm/middleware/checkbox.py
CHANGED
|
@@ -6,7 +6,7 @@ This module defines the Checkbox class, which is a subclass of the
|
|
|
6
6
|
Widget class. It represents a checkbox form field in a PDF document.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from typing import Union
|
|
9
|
+
from typing import Any, Union
|
|
10
10
|
|
|
11
11
|
from .base import Widget
|
|
12
12
|
|
|
@@ -44,6 +44,23 @@ class Checkbox(Widget):
|
|
|
44
44
|
|
|
45
45
|
self.size: float = None
|
|
46
46
|
|
|
47
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Custom attribute setter for the Checkbox class.
|
|
50
|
+
|
|
51
|
+
If the attribute being set is 'size', it sets both 'width' and 'height'
|
|
52
|
+
attributes of the widget to the given value, ensuring the checkbox remains
|
|
53
|
+
square. For all other attributes, it defers to the parent class's setter.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
name (str): The name of the attribute to set.
|
|
57
|
+
value (Any): The value to set for the attribute.
|
|
58
|
+
"""
|
|
59
|
+
if name == "size":
|
|
60
|
+
super().__setattr__("width", value)
|
|
61
|
+
super().__setattr__("height", value)
|
|
62
|
+
super().__setattr__(name, value)
|
|
63
|
+
|
|
47
64
|
@property
|
|
48
65
|
def schema_definition(self) -> dict:
|
|
49
66
|
"""
|
|
@@ -6,7 +6,6 @@ This module defines the Signature class, which is a subclass of the
|
|
|
6
6
|
Widget class. It represents a signature form field in a PDF document,
|
|
7
7
|
allowing users to add their signature as an image.
|
|
8
8
|
"""
|
|
9
|
-
# TODO: In the `stream` property, `fp_or_f_obj_or_stream_to_stream` is called every time the property is accessed. If the signature image is large or the property is accessed frequently, consider caching the result of `fp_or_f_obj_or_stream_to_stream` to avoid redundant file reads.
|
|
10
9
|
|
|
11
10
|
from os.path import expanduser
|
|
12
11
|
from typing import Union
|
PyPDFForm/patterns.py
CHANGED
|
@@ -7,18 +7,15 @@ checkboxes, radio buttons, dropdowns, images, and signatures) based on their
|
|
|
7
7
|
properties in the PDF's annotation dictionary. It also provides utility functions
|
|
8
8
|
for updating these widgets.
|
|
9
9
|
"""
|
|
10
|
-
# TODO: The `WIDGET_TYPE_PATTERNS` list is iterated through to determine widget types. For very large numbers of annotations or complex pattern matching, consider optimizing this lookup, perhaps by pre-compiling regexes or using a more efficient data structure if the patterns allow.
|
|
11
|
-
# TODO: In `update_checkbox_value` and `update_radio_value`, iterating through `annot[AP][N]` to find the correct appearance state might be slow if `N` contains many entries. If possible, a direct lookup or a more optimized search could improve performance.
|
|
12
|
-
# TODO: In `update_dropdown_value`, the list comprehension for `ArrayObject` can be computationally intensive for dropdowns with many choices, as it creates new `TextStringObject` and `ArrayObject` instances for each choice. Consider optimizing this if dropdowns have a very large number of options.
|
|
13
|
-
# TODO: The `get_checkbox_value` and `get_radio_value` functions involve dictionary lookups and comparisons. While generally fast, repeated calls in a tight loop for many widgets could accumulate overhead.
|
|
14
10
|
|
|
15
|
-
from typing import Union
|
|
11
|
+
from typing import Tuple, Union
|
|
16
12
|
|
|
17
13
|
from pypdf.generic import (ArrayObject, DictionaryObject, NameObject,
|
|
18
14
|
NumberObject, TextStringObject)
|
|
19
15
|
|
|
20
|
-
from .constants import (AP, AS, DV, FT, IMAGE_FIELD_IDENTIFIER, JS,
|
|
21
|
-
A, Btn, Ch, I, N, Off, Opt, Parent,
|
|
16
|
+
from .constants import (AP, AS, DV, FT, IMAGE_FIELD_IDENTIFIER, JS, MULTILINE,
|
|
17
|
+
SLASH, TU, A, Btn, Ch, Ff, I, N, Off, Opt, Parent,
|
|
18
|
+
Rect, Sig, T, Tx, V, Yes)
|
|
22
19
|
from .middleware.checkbox import Checkbox
|
|
23
20
|
from .middleware.dropdown import Dropdown
|
|
24
21
|
from .middleware.image import Image
|
|
@@ -178,7 +175,9 @@ def get_radio_value(annot: DictionaryObject) -> bool:
|
|
|
178
175
|
return False
|
|
179
176
|
|
|
180
177
|
|
|
181
|
-
def update_dropdown_value(
|
|
178
|
+
def update_dropdown_value(
|
|
179
|
+
annot: DictionaryObject, widget: Dropdown, need_appearances: bool
|
|
180
|
+
) -> None:
|
|
182
181
|
"""
|
|
183
182
|
Updates the value of a dropdown annotation, selecting an option from the list.
|
|
184
183
|
|
|
@@ -189,17 +188,21 @@ def update_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
|
|
|
189
188
|
Args:
|
|
190
189
|
annot (DictionaryObject): The dropdown annotation dictionary.
|
|
191
190
|
widget (Dropdown): The Dropdown widget object containing the selected value.
|
|
191
|
+
need_appearances (bool): If True, skips updating the appearance stream (AP) to
|
|
192
|
+
maintain compatibility with Adobe Reader's behavior for certain fields.
|
|
192
193
|
"""
|
|
193
194
|
choices = widget.choices or []
|
|
194
195
|
if Parent in annot and T not in annot:
|
|
195
196
|
annot[NameObject(Parent)][NameObject(V)] = TextStringObject(
|
|
196
197
|
choices[widget.value]
|
|
197
198
|
)
|
|
198
|
-
|
|
199
|
+
if not need_appearances:
|
|
200
|
+
annot[NameObject(AP)] = TextStringObject(choices[widget.value])
|
|
199
201
|
else:
|
|
200
202
|
annot[NameObject(V)] = TextStringObject(choices[widget.value])
|
|
201
|
-
annot[NameObject(AP)] = TextStringObject(choices[widget.value])
|
|
202
203
|
annot[NameObject(I)] = ArrayObject([NumberObject(widget.value)])
|
|
204
|
+
if not need_appearances:
|
|
205
|
+
annot[NameObject(AP)] = TextStringObject(choices[widget.value])
|
|
203
206
|
|
|
204
207
|
|
|
205
208
|
def get_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
|
|
@@ -225,7 +228,9 @@ def get_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
|
|
|
225
228
|
widget.value = i or None # set None when 0
|
|
226
229
|
|
|
227
230
|
|
|
228
|
-
def update_text_value(
|
|
231
|
+
def update_text_value(
|
|
232
|
+
annot: DictionaryObject, widget: Text, need_appearances: bool
|
|
233
|
+
) -> None:
|
|
229
234
|
"""
|
|
230
235
|
Updates the value of a text annotation, setting the text content.
|
|
231
236
|
|
|
@@ -235,13 +240,17 @@ def update_text_value(annot: DictionaryObject, widget: Text) -> None:
|
|
|
235
240
|
Args:
|
|
236
241
|
annot (DictionaryObject): The text annotation dictionary.
|
|
237
242
|
widget (Text): The Text widget object containing the text value.
|
|
243
|
+
need_appearances (bool): If True, skips updating the appearance stream (AP) to
|
|
244
|
+
maintain compatibility with Adobe Reader's behavior for certain fields.
|
|
238
245
|
"""
|
|
239
246
|
if Parent in annot and T not in annot:
|
|
240
247
|
annot[NameObject(Parent)][NameObject(V)] = TextStringObject(widget.value)
|
|
241
|
-
|
|
248
|
+
if not need_appearances:
|
|
249
|
+
annot[NameObject(AP)] = TextStringObject(widget.value)
|
|
242
250
|
else:
|
|
243
251
|
annot[NameObject(V)] = TextStringObject(widget.value)
|
|
244
|
-
|
|
252
|
+
if not need_appearances:
|
|
253
|
+
annot[NameObject(AP)] = TextStringObject(widget.value)
|
|
245
254
|
|
|
246
255
|
|
|
247
256
|
def get_text_value(annot: DictionaryObject, widget: Text) -> None:
|
|
@@ -277,3 +286,52 @@ def update_annotation_name(annot: DictionaryObject, val: str) -> None:
|
|
|
277
286
|
annot[NameObject(Parent)][NameObject(T)] = TextStringObject(val)
|
|
278
287
|
else:
|
|
279
288
|
annot[NameObject(T)] = TextStringObject(val)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_text_field_multiline(annot: DictionaryObject) -> bool:
|
|
292
|
+
"""
|
|
293
|
+
Checks if a text field annotation is multiline.
|
|
294
|
+
|
|
295
|
+
This function inspects the 'Ff' (field flags) entry of the text annotation
|
|
296
|
+
dictionary (or its parent if it's a child annotation) to determine if the
|
|
297
|
+
Multiline flag is set.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
annot (DictionaryObject): The text annotation dictionary.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
bool: True if the text field is multiline, False otherwise.
|
|
304
|
+
"""
|
|
305
|
+
if Parent in annot and Ff not in annot:
|
|
306
|
+
return bool(
|
|
307
|
+
int(
|
|
308
|
+
annot[NameObject(Parent)][NameObject(Ff)]
|
|
309
|
+
if Ff in annot[NameObject(Parent)]
|
|
310
|
+
else 0
|
|
311
|
+
)
|
|
312
|
+
& MULTILINE
|
|
313
|
+
)
|
|
314
|
+
return bool(int(annot[NameObject(Ff)] if Ff in annot else 0) & MULTILINE)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_field_rect(annot: DictionaryObject) -> Tuple[float, float, float, float]:
|
|
318
|
+
"""
|
|
319
|
+
Retrieves the normalized rectangular bounding box of a field annotation.
|
|
320
|
+
|
|
321
|
+
The PDF 'Rect' entry contains [llx, lly, urx, ury] (lower-left x, y, upper-right x, y).
|
|
322
|
+
This function converts it to a normalized tuple of (x, y, width, height) in float format.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
annot (DictionaryObject): The annotation dictionary containing the 'Rect' key.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
tuple: A tuple (x, y, width, height) representing the field's bounding box.
|
|
329
|
+
"""
|
|
330
|
+
rect = annot[Rect]
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
float(rect[0]),
|
|
334
|
+
float(rect[1]),
|
|
335
|
+
float(abs(rect[2] - rect[0])),
|
|
336
|
+
float(abs(rect[3] - rect[1])),
|
|
337
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
The `raw` package provides classes representing raw drawable elements
|
|
4
|
+
(like text and images) that can be rendered directly onto a PDF document.
|
|
5
|
+
|
|
6
|
+
It defines `RawTypes` as a Union of all supported raw element types, used for
|
|
7
|
+
type hinting in methods that handle drawing onto the PDF.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
from .circle import RawCircle
|
|
14
|
+
from .ellipse import RawEllipse
|
|
15
|
+
from .image import RawImage
|
|
16
|
+
from .line import RawLine
|
|
17
|
+
from .rect import RawRectangle
|
|
18
|
+
from .text import RawText
|
|
19
|
+
|
|
20
|
+
RawTypes = Union[RawText, RawImage, RawLine, RawRectangle, RawCircle, RawEllipse]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class RawElements:
|
|
25
|
+
"""
|
|
26
|
+
A container class that provides convenient access to all available raw drawable elements.
|
|
27
|
+
|
|
28
|
+
This class acts as a namespace for the various `Raw` classes defined in the
|
|
29
|
+
`PyPDFForm.raw` package, making it easier to reference them (e.g., `RawElements.RawText`).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
RawText = RawText
|
|
33
|
+
RawImage = RawImage
|
|
34
|
+
RawLine = RawLine
|
|
35
|
+
RawRectangle = RawRectangle
|
|
36
|
+
RawCircle = RawCircle
|
|
37
|
+
RawEllipse = RawEllipse
|
PyPDFForm/raw/circle.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Contains the RawCircle class, which represents a circle that can be drawn
|
|
4
|
+
directly onto a PDF page at specified coordinates and radius.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..constants import DEFAULT_FONT_COLOR
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RawCircle:
|
|
11
|
+
"""
|
|
12
|
+
Represents a circle object intended for direct drawing onto a specific page
|
|
13
|
+
of a PDF document at specified coordinates and radius.
|
|
14
|
+
|
|
15
|
+
This class encapsulates the necessary information (center position, radius,
|
|
16
|
+
color, and fill color) to render a circle on a PDF page.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
page_number: int,
|
|
22
|
+
center_x: float,
|
|
23
|
+
center_y: float,
|
|
24
|
+
radius: float,
|
|
25
|
+
color: tuple = DEFAULT_FONT_COLOR,
|
|
26
|
+
fill_color: tuple = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initializes a raw circle object for drawing.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
page_number: The 1-based index of the page where the circle should be drawn.
|
|
33
|
+
center_x: The x-coordinate (horizontal position) of the center of the circle.
|
|
34
|
+
center_y: The y-coordinate (vertical position) of the center of the circle.
|
|
35
|
+
radius: The radius of the circle.
|
|
36
|
+
color: The color of the circle's outline as an RGB tuple (0-1 for each channel).
|
|
37
|
+
fill_color: The fill color of the circle as an RGB tuple (0-1 for each channel).
|
|
38
|
+
"""
|
|
39
|
+
super().__init__()
|
|
40
|
+
|
|
41
|
+
self.page_number = page_number
|
|
42
|
+
self.center_x = center_x
|
|
43
|
+
self.center_y = center_y
|
|
44
|
+
self.radius = radius
|
|
45
|
+
self.color = color
|
|
46
|
+
self.fill_color = fill_color
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def to_draw(self) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
Converts the raw circle object into a dictionary format ready for drawing.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A dictionary containing drawing parameters: page number, object type ("circle"),
|
|
55
|
+
center coordinates, radius, outline color, and fill color.
|
|
56
|
+
"""
|
|
57
|
+
return {
|
|
58
|
+
"page_number": self.page_number,
|
|
59
|
+
"type": "circle",
|
|
60
|
+
"center_x": self.center_x,
|
|
61
|
+
"center_y": self.center_y,
|
|
62
|
+
"radius": self.radius,
|
|
63
|
+
"color": self.color,
|
|
64
|
+
"fill_color": self.fill_color,
|
|
65
|
+
}
|