PyPDFForm 3.5.3__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 +153 -30
- 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 +44 -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 -12
- 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.3.dist-info → pypdfform-4.2.0.dist-info}/METADATA +33 -26
- pypdfform-4.2.0.dist-info/RECORD +47 -0
- {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/licenses/LICENSE +1 -1
- pypdfform-3.5.3.dist-info/RECORD +0 -35
- /PyPDFForm/{widgets → assets}/bedrock.py +0 -0
- {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/WHEEL +0 -0
- {pypdfform-3.5.3.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
|
|
|
@@ -216,9 +215,7 @@ def update_text_field_multiline(annot: DictionaryObject, val: bool) -> None:
|
|
|
216
215
|
val (bool): True to enable multiline, False to disable.
|
|
217
216
|
"""
|
|
218
217
|
if val:
|
|
219
|
-
#
|
|
220
|
-
# may need to change everywhere how feature flags precedence work
|
|
221
|
-
# https://github.com/chinapandaman/PyPDFForm/issues/1162#issuecomment-3326233842
|
|
218
|
+
# Ff in annot[Parent] only in hooks.py, or when editing instead of retrieving
|
|
222
219
|
if Parent in annot and Ff in annot[Parent]:
|
|
223
220
|
annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
|
|
224
221
|
int(
|
|
@@ -247,7 +244,7 @@ def update_text_field_comb(annot: DictionaryObject, val: bool) -> None:
|
|
|
247
244
|
val (bool): True to enable comb, False to disable.
|
|
248
245
|
"""
|
|
249
246
|
if val:
|
|
250
|
-
if Parent in annot and Ff
|
|
247
|
+
if Parent in annot and Ff in annot[Parent]:
|
|
251
248
|
annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
|
|
252
249
|
int(
|
|
253
250
|
annot[NameObject(Parent)][NameObject(Ff)]
|
|
@@ -367,12 +364,12 @@ def flatten_generic(annot: DictionaryObject, val: bool) -> None:
|
|
|
367
364
|
annot (DictionaryObject): The annotation dictionary.
|
|
368
365
|
val (bool): True to flatten (make read-only), False to unflatten (make editable).
|
|
369
366
|
"""
|
|
370
|
-
if Parent in annot and Ff not in annot:
|
|
367
|
+
if Parent in annot and (Ff in annot[Parent] or Ff not in annot):
|
|
371
368
|
annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
|
|
372
369
|
(
|
|
373
|
-
int(annot.get(NameObject(Ff), 0)) | READ_ONLY
|
|
370
|
+
int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) | READ_ONLY
|
|
374
371
|
if val
|
|
375
|
-
else int(annot.get(NameObject(Ff), 0)) & ~READ_ONLY
|
|
372
|
+
else int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) & ~READ_ONLY
|
|
376
373
|
)
|
|
377
374
|
)
|
|
378
375
|
else:
|
|
@@ -412,20 +409,146 @@ def update_field_required(annot: DictionaryObject, val: bool) -> None:
|
|
|
412
409
|
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
413
410
|
val (bool): True to set the field as required, False to make it optional.
|
|
414
411
|
"""
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
# )
|
|
423
|
-
# )
|
|
424
|
-
# else:
|
|
425
|
-
annot[NameObject(Ff)] = NumberObject(
|
|
426
|
-
(
|
|
427
|
-
int(annot.get(NameObject(Ff), 0)) | REQUIRED
|
|
428
|
-
if val
|
|
429
|
-
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
|
+
)
|
|
430
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
|
|
431
452
|
)
|
|
453
|
+
annot[NameObject(AA)][NameObject(trigger_event)][NameObject(S)] = NameObject(
|
|
454
|
+
JavaScript
|
|
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,19 +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
16
|
from .constants import (AP, AS, DV, FT, IMAGE_FIELD_IDENTIFIER, JS, MULTILINE,
|
|
21
|
-
SLASH, TU, A, Btn, Ch, Ff, I, N, Off, Opt, Parent,
|
|
22
|
-
T, Tx, V, Yes)
|
|
17
|
+
SLASH, TU, A, Btn, Ch, Ff, I, N, Off, Opt, Parent,
|
|
18
|
+
Rect, Sig, T, Tx, V, Yes)
|
|
23
19
|
from .middleware.checkbox import Checkbox
|
|
24
20
|
from .middleware.dropdown import Dropdown
|
|
25
21
|
from .middleware.image import Image
|
|
@@ -179,7 +175,9 @@ def get_radio_value(annot: DictionaryObject) -> bool:
|
|
|
179
175
|
return False
|
|
180
176
|
|
|
181
177
|
|
|
182
|
-
def update_dropdown_value(
|
|
178
|
+
def update_dropdown_value(
|
|
179
|
+
annot: DictionaryObject, widget: Dropdown, need_appearances: bool
|
|
180
|
+
) -> None:
|
|
183
181
|
"""
|
|
184
182
|
Updates the value of a dropdown annotation, selecting an option from the list.
|
|
185
183
|
|
|
@@ -190,17 +188,21 @@ def update_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
|
|
|
190
188
|
Args:
|
|
191
189
|
annot (DictionaryObject): The dropdown annotation dictionary.
|
|
192
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.
|
|
193
193
|
"""
|
|
194
194
|
choices = widget.choices or []
|
|
195
195
|
if Parent in annot and T not in annot:
|
|
196
196
|
annot[NameObject(Parent)][NameObject(V)] = TextStringObject(
|
|
197
197
|
choices[widget.value]
|
|
198
198
|
)
|
|
199
|
-
|
|
199
|
+
if not need_appearances:
|
|
200
|
+
annot[NameObject(AP)] = TextStringObject(choices[widget.value])
|
|
200
201
|
else:
|
|
201
202
|
annot[NameObject(V)] = TextStringObject(choices[widget.value])
|
|
202
|
-
annot[NameObject(AP)] = TextStringObject(choices[widget.value])
|
|
203
203
|
annot[NameObject(I)] = ArrayObject([NumberObject(widget.value)])
|
|
204
|
+
if not need_appearances:
|
|
205
|
+
annot[NameObject(AP)] = TextStringObject(choices[widget.value])
|
|
204
206
|
|
|
205
207
|
|
|
206
208
|
def get_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
|
|
@@ -226,7 +228,9 @@ def get_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
|
|
|
226
228
|
widget.value = i or None # set None when 0
|
|
227
229
|
|
|
228
230
|
|
|
229
|
-
def update_text_value(
|
|
231
|
+
def update_text_value(
|
|
232
|
+
annot: DictionaryObject, widget: Text, need_appearances: bool
|
|
233
|
+
) -> None:
|
|
230
234
|
"""
|
|
231
235
|
Updates the value of a text annotation, setting the text content.
|
|
232
236
|
|
|
@@ -236,13 +240,17 @@ def update_text_value(annot: DictionaryObject, widget: Text) -> None:
|
|
|
236
240
|
Args:
|
|
237
241
|
annot (DictionaryObject): The text annotation dictionary.
|
|
238
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.
|
|
239
245
|
"""
|
|
240
246
|
if Parent in annot and T not in annot:
|
|
241
247
|
annot[NameObject(Parent)][NameObject(V)] = TextStringObject(widget.value)
|
|
242
|
-
|
|
248
|
+
if not need_appearances:
|
|
249
|
+
annot[NameObject(AP)] = TextStringObject(widget.value)
|
|
243
250
|
else:
|
|
244
251
|
annot[NameObject(V)] = TextStringObject(widget.value)
|
|
245
|
-
|
|
252
|
+
if not need_appearances:
|
|
253
|
+
annot[NameObject(AP)] = TextStringObject(widget.value)
|
|
246
254
|
|
|
247
255
|
|
|
248
256
|
def get_text_value(annot: DictionaryObject, widget: Text) -> None:
|
|
@@ -304,3 +312,26 @@ def get_text_field_multiline(annot: DictionaryObject) -> bool:
|
|
|
304
312
|
& MULTILINE
|
|
305
313
|
)
|
|
306
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
|
+
}
|
PyPDFForm/raw/ellipse.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Contains the RawEllipse class, which represents an ellipse that can be drawn
|
|
4
|
+
directly onto a PDF page defined by its bounding box.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..constants import DEFAULT_FONT_COLOR
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RawEllipse:
|
|
11
|
+
"""
|
|
12
|
+
Represents an ellipse object intended for direct drawing onto a specific page
|
|
13
|
+
of a PDF document defined by its bounding box coordinates.
|
|
14
|
+
|
|
15
|
+
This class encapsulates the necessary information (bounding box corners,
|
|
16
|
+
page number, color, and fill color) to render an ellipse on a PDF page.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
page_number: int,
|
|
22
|
+
x1: float,
|
|
23
|
+
y1: float,
|
|
24
|
+
x2: float,
|
|
25
|
+
y2: float,
|
|
26
|
+
color: tuple = DEFAULT_FONT_COLOR,
|
|
27
|
+
fill_color: tuple = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initializes a raw ellipse object for drawing.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
page_number: The 1-based index of the page where the ellipse should be drawn.
|
|
34
|
+
x1: The x-coordinate of the first corner of the bounding box.
|
|
35
|
+
y1: The y-coordinate of the first corner of the bounding box.
|
|
36
|
+
x2: The x-coordinate of the second corner of the bounding box.
|
|
37
|
+
y2: The y-coordinate of the second corner of the bounding box.
|
|
38
|
+
color: The color of the ellipse's outline as an RGB tuple (0-1 for each channel).
|
|
39
|
+
fill_color: The fill color of the ellipse as an RGB tuple (0-1 for each channel).
|
|
40
|
+
"""
|
|
41
|
+
super().__init__()
|
|
42
|
+
|
|
43
|
+
self.page_number = page_number
|
|
44
|
+
self.x1 = x1
|
|
45
|
+
self.y1 = y1
|
|
46
|
+
self.x2 = x2
|
|
47
|
+
self.y2 = y2
|
|
48
|
+
self.color = color
|
|
49
|
+
self.fill_color = fill_color
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def to_draw(self) -> dict:
|
|
53
|
+
"""
|
|
54
|
+
Converts the raw ellipse object into a dictionary format ready for drawing.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A dictionary containing drawing parameters: page number, object type ("ellipse"),
|
|
58
|
+
bounding box coordinates, outline color, and fill color.
|
|
59
|
+
"""
|
|
60
|
+
return {
|
|
61
|
+
"page_number": self.page_number,
|
|
62
|
+
"type": "ellipse",
|
|
63
|
+
"x1": self.x1,
|
|
64
|
+
"y1": self.y1,
|
|
65
|
+
"x2": self.x2,
|
|
66
|
+
"y2": self.y2,
|
|
67
|
+
"color": self.color,
|
|
68
|
+
"fill_color": self.fill_color,
|
|
69
|
+
}
|