PyPDFForm 2.3.3__tar.gz → 2.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of PyPDFForm might be problematic. Click here for more details.

Files changed (55) hide show
  1. {pypdfform-2.3.3 → pypdfform-2.5.0}/PKG-INFO +2 -3
  2. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/filler.py +3 -1
  4. pypdfform-2.5.0/PyPDFForm/hooks.py +216 -0
  5. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/middleware/base.py +24 -0
  6. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/middleware/checkbox.py +4 -0
  7. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/middleware/text.py +5 -0
  8. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/patterns.py +3 -38
  9. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/base.py +1 -1
  10. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/text.py +1 -1
  11. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/wrapper.py +84 -35
  12. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm.egg-info/PKG-INFO +2 -3
  13. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm.egg-info/SOURCES.txt +3 -1
  14. {pypdfform-2.3.3 → pypdfform-2.5.0}/pyproject.toml +2 -2
  15. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_adobe_mode.py +4 -4
  16. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_create_widget.py +76 -54
  17. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_dropdown.py +12 -12
  18. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_dropdown_simple.py +6 -6
  19. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_fill_max_length_text_field.py +16 -16
  20. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_fill_max_length_text_field_simple.py +14 -14
  21. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_functional.py +50 -50
  22. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_functional_simple.py +9 -9
  23. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_paragraph.py +40 -40
  24. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_paragraph_simple.py +9 -9
  25. pypdfform-2.5.0/tests/test_widget_attr_trigger.py +153 -0
  26. {pypdfform-2.3.3 → pypdfform-2.5.0}/LICENSE +0 -0
  27. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/adapter.py +0 -0
  28. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/constants.py +0 -0
  29. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/coordinate.py +0 -0
  30. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/font.py +0 -0
  31. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/image.py +0 -0
  32. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/middleware/__init__.py +0 -0
  33. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/middleware/dropdown.py +0 -0
  34. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/middleware/image.py +0 -0
  35. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/middleware/radio.py +0 -0
  36. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/middleware/signature.py +0 -0
  37. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/template.py +0 -0
  38. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/utils.py +0 -0
  39. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/watermark.py +0 -0
  40. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/__init__.py +0 -0
  41. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/bedrock.py +0 -0
  42. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/checkbox.py +0 -0
  43. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/dropdown.py +0 -0
  44. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/image.py +0 -0
  45. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/radio.py +0 -0
  46. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm/widgets/signature.py +0 -0
  47. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  48. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm.egg-info/requires.txt +0 -0
  49. {pypdfform-2.3.3 → pypdfform-2.5.0}/PyPDFForm.egg-info/top_level.txt +0 -0
  50. {pypdfform-2.3.3 → pypdfform-2.5.0}/README.md +0 -0
  51. {pypdfform-2.3.3 → pypdfform-2.5.0}/setup.cfg +0 -0
  52. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_fill_method.py +0 -0
  53. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_preview.py +0 -0
  54. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_signature.py +0 -0
  55. {pypdfform-2.3.3 → pypdfform-2.5.0}/tests/test_use_full_widget_name.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 2.3.3
3
+ Version: 2.5.0
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -10,7 +10,6 @@ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
13
- Classifier: Programming Language :: Python :: 3.8
14
13
  Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
@@ -18,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Operating System :: OS Independent
20
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
- Requires-Python: >=3.8
20
+ Requires-Python: >=3.9
22
21
  Description-Content-Type: text/markdown
23
22
  License-File: LICENSE
24
23
  Requires-Dist: cryptography
@@ -5,7 +5,7 @@ This package provides tools for filling PDF forms, drawing text and images,
5
5
  and manipulating PDF form elements programmatically.
6
6
  """
7
7
 
8
- __version__ = "2.3.3"
8
+ __version__ = "2.5.0"
9
9
 
10
10
  from .wrapper import FormWrapper, PdfWrapper
11
11
 
@@ -337,6 +337,7 @@ def enable_adobe_mode(reader: PdfReader, writer: PdfWriter, adobe_mode: bool) ->
337
337
  def simple_fill(
338
338
  template: bytes,
339
339
  widgets: Dict[str, WIDGET_TYPES],
340
+ use_full_widget_name: bool,
340
341
  flatten: bool = False,
341
342
  adobe_mode: bool = False,
342
343
  ) -> bytes:
@@ -350,6 +351,7 @@ def simple_fill(
350
351
  Args:
351
352
  template: Input PDF form as bytes
352
353
  widgets: Dictionary mapping field names to widget middleware
354
+ use_full_widget_name: If True, uses the full widget name as the key in the widgets dictionary
353
355
  flatten: If True, makes form fields read-only
354
356
  adobe_mode: If True, enables Adobe Acrobat compatibility
355
357
 
@@ -367,7 +369,7 @@ def simple_fill(
367
369
  for page in out.pages:
368
370
  for annot in page.get(Annots, []):
369
371
  annot = cast(DictionaryObject, annot.get_object())
370
- key = get_widget_key(annot.get_object(), False)
372
+ key = get_widget_key(annot.get_object(), use_full_widget_name)
371
373
 
372
374
  widget = widgets.get(key)
373
375
  if widget is None or widget.value is None:
@@ -0,0 +1,216 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Module containing hook functions for PDF form widget manipulation.
3
+
4
+ This module provides functions to apply various transformations and modifications
5
+ to PDF form widgets through a hook system. It allows dynamic modification of
6
+ widget properties like font sizes and other attributes.
7
+ """
8
+
9
+ import sys
10
+ from io import BytesIO
11
+ from typing import cast
12
+
13
+ from pypdf import PdfReader, PdfWriter
14
+ from pypdf.generic import (ArrayObject, DictionaryObject, FloatObject,
15
+ NameObject, NumberObject, TextStringObject)
16
+
17
+ from .constants import (COMB, DA, FONT_COLOR_IDENTIFIER, FONT_SIZE_IDENTIFIER,
18
+ MULTILINE, Annots, Ff, Parent, Q, Rect)
19
+ from .template import get_widget_key
20
+ from .utils import stream_to_io
21
+
22
+
23
+ def trigger_widget_hooks(
24
+ pdf: bytes,
25
+ widgets: dict,
26
+ use_full_widget_name: bool,
27
+ ) -> bytes:
28
+ """Apply all registered widget hooks to a PDF document.
29
+
30
+ Args:
31
+ pdf: The input PDF document as bytes
32
+ widgets: Dictionary mapping widget names to widget objects
33
+ use_full_widget_name: Whether to use full widget names including parent hierarchy
34
+
35
+ Returns:
36
+ The modified PDF document as bytes
37
+
38
+ Note:
39
+ This function processes all pages and annotations in the PDF, applying
40
+ any hooks registered in the widget objects.
41
+ """
42
+
43
+ pdf_file = PdfReader(stream_to_io(pdf))
44
+ output = PdfWriter()
45
+ output.append(pdf_file)
46
+
47
+ for page in output.pages:
48
+ for annot in page.get(Annots, []):
49
+ annot = cast(DictionaryObject, annot.get_object())
50
+ key = get_widget_key(annot.get_object(), use_full_widget_name)
51
+
52
+ widget = widgets.get(key)
53
+ if widget is None or not widget.hooks_to_trigger:
54
+ continue
55
+
56
+ for hook in widget.hooks_to_trigger:
57
+ getattr(sys.modules[__name__], hook[0])(annot, hook[1])
58
+
59
+ for widget in widgets.values():
60
+ widget.hooks_to_trigger = []
61
+
62
+ with BytesIO() as f:
63
+ output.write(f)
64
+ f.seek(0)
65
+ return f.read()
66
+
67
+
68
+ def update_text_field_font_size(annot: DictionaryObject, val: float) -> None:
69
+ """Update the font size of a text field widget.
70
+
71
+ Args:
72
+ annot: The PDF annotation (widget) dictionary object
73
+ val: The new font size value to apply
74
+
75
+ Note:
76
+ Handles both direct font size specification and inherited font sizes
77
+ from parent objects. Modifies the DA (default appearance) string.
78
+ """
79
+
80
+ if Parent in annot and DA not in annot:
81
+ text_appearance = annot[Parent][DA]
82
+ else:
83
+ text_appearance = annot[DA]
84
+
85
+ text_appearance = text_appearance.split(" ")
86
+ font_size_index = 0
87
+ for i, value in enumerate(text_appearance):
88
+ if value.startswith(FONT_SIZE_IDENTIFIER):
89
+ font_size_index = i - 1
90
+ break
91
+
92
+ text_appearance[font_size_index] = str(val)
93
+ new_text_appearance = " ".join(text_appearance)
94
+
95
+ if Parent in annot and DA not in annot:
96
+ annot[NameObject(Parent)][NameObject(DA)] = TextStringObject(
97
+ new_text_appearance
98
+ )
99
+ else:
100
+ annot[NameObject(DA)] = TextStringObject(new_text_appearance)
101
+
102
+
103
+ def update_text_field_font_color(annot: DictionaryObject, val: tuple) -> None:
104
+ """Update the font color of a text field widget.
105
+
106
+ Args:
107
+ annot: The PDF annotation (widget) dictionary object
108
+ val: Tuple containing RGB color values (e.g. (1, 0, 0) for red)
109
+
110
+ Note:
111
+ Handles both direct font color specification and inherited font colors
112
+ from parent objects. Modifies the DA (default appearance) string to
113
+ include the new color values after the font size identifier.
114
+ """
115
+
116
+ if Parent in annot and DA not in annot:
117
+ text_appearance = annot[Parent][DA]
118
+ else:
119
+ text_appearance = annot[DA]
120
+
121
+ text_appearance = text_appearance.split(" ")
122
+ font_size_identifier_index = 0
123
+ for i, value in enumerate(text_appearance):
124
+ if value == FONT_SIZE_IDENTIFIER:
125
+ font_size_identifier_index = i
126
+ break
127
+
128
+ new_text_appearance = (
129
+ text_appearance[:font_size_identifier_index]
130
+ + [FONT_SIZE_IDENTIFIER]
131
+ + [str(each) for each in val]
132
+ )
133
+ new_text_appearance = " ".join(new_text_appearance) + FONT_COLOR_IDENTIFIER
134
+
135
+ if Parent in annot and DA not in annot:
136
+ annot[NameObject(Parent)][NameObject(DA)] = TextStringObject(
137
+ new_text_appearance
138
+ )
139
+ else:
140
+ annot[NameObject(DA)] = TextStringObject(new_text_appearance)
141
+
142
+
143
+ def update_text_field_alignment(annot: DictionaryObject, val: int) -> None:
144
+ """Update text alignment for text field annotations.
145
+
146
+ Modifies the alignment (Q) field of a text field annotation to set the
147
+ specified text alignment.
148
+
149
+ Args:
150
+ annot: PDF text field annotation dictionary to modify
151
+ val: Alignment value to set (typically 0=left, 1=center, 2=right)
152
+ """
153
+
154
+ annot[NameObject(Q)] = NumberObject(val)
155
+
156
+
157
+ def update_text_field_multiline(annot: DictionaryObject, val: bool) -> None:
158
+ """Update multiline flag for text field annotations.
159
+
160
+ Modifies the field flags (Ff) of a text field annotation to set or
161
+ clear the multiline flag based on the input value.
162
+
163
+ Args:
164
+ annot: PDF text field annotation dictionary to modify
165
+ val: Whether to enable multiline (True) or disable (False)
166
+ """
167
+
168
+ if val:
169
+ annot[NameObject(Ff)] = NumberObject(int(annot[NameObject(Ff)]) | MULTILINE)
170
+
171
+
172
+ def update_text_field_comb(annot: DictionaryObject, val: bool) -> None:
173
+ """Update comb formatting flag for text field annotations.
174
+
175
+ Modifies the field flags (Ff) of a text field annotation to set or
176
+ clear the comb flag which enables/disables comb formatting.
177
+
178
+ Args:
179
+ annot: PDF text field annotation dictionary to modify
180
+ val: Whether to enable comb formatting (True) or disable (False)
181
+ """
182
+
183
+ if val:
184
+ annot[NameObject(Ff)] = NumberObject(int(annot[NameObject(Ff)]) | COMB)
185
+
186
+
187
+ def update_check_radio_size(annot: DictionaryObject, val: float) -> None:
188
+ """Update the size of a checkbox or radio button widget while maintaining center position.
189
+
190
+ Args:
191
+ annot: PDF annotation dictionary containing the widget to modify
192
+ val: New size value (width and height) for the widget
193
+
194
+ Note:
195
+ The widget will be resized symmetrically around its center point,
196
+ maintaining the same center position while changing its dimensions.
197
+ """
198
+
199
+ rect = annot[Rect]
200
+ center_x = (rect[0] + rect[2]) / 2
201
+ center_y = (rect[1] + rect[3]) / 2
202
+ new_rect = [
203
+ FloatObject(center_x - val / 2),
204
+ FloatObject(center_y - val / 2),
205
+ FloatObject(center_x + val / 2),
206
+ FloatObject(center_y + val / 2),
207
+ ]
208
+ annot[NameObject(Rect)] = ArrayObject(new_rect)
209
+
210
+
211
+ # TODO: remove this and switch to hooks
212
+ NON_ACRO_FORM_PARAM_TO_FUNC = {
213
+ ("TextWidget", "alignment"): update_text_field_alignment,
214
+ ("TextWidget", "multiline"): update_text_field_multiline,
215
+ ("TextWidget", "comb"): update_text_field_comb,
216
+ }
@@ -26,6 +26,8 @@ class Widget:
26
26
  - Any widget-specific functionality
27
27
  """
28
28
 
29
+ SET_ATTR_TRIGGER_HOOK_MAP = {}
30
+
29
31
  def __init__(
30
32
  self,
31
33
  name: str,
@@ -48,6 +50,28 @@ class Widget:
48
50
  self.border_style = None
49
51
  self.dash_array = None
50
52
  self.render_widget = None
53
+ self.hooks_to_trigger = []
54
+
55
+ def __setattr__(self, name: str, value: Any) -> None:
56
+ """Sets an attribute on the widget with special handling for hook-triggering attributes.
57
+
58
+ For attributes listed in SET_ATTR_TRIGGER_HOOK_MAP, when set to non-None values,
59
+ adds a hook to hooks_to_trigger list with the mapped hook name and value.
60
+ All other attributes are set normally via the standard object.__setattr__.
61
+
62
+ Args:
63
+ name: Name of the attribute to set
64
+ value: Value to set the attribute to
65
+
66
+ Note:
67
+ The hook triggering only occurs when:
68
+ 1. The attribute is in SET_ATTR_TRIGGER_HOOK_MAP
69
+ 2. The value being set is not None
70
+ """
71
+
72
+ if name in self.SET_ATTR_TRIGGER_HOOK_MAP and value is not None:
73
+ self.hooks_to_trigger.append((self.SET_ATTR_TRIGGER_HOOK_MAP[name], value))
74
+ super().__setattr__(name, value)
51
75
 
52
76
  @property
53
77
  def name(self) -> str:
@@ -25,6 +25,10 @@ class Checkbox(Widget):
25
25
  Inherits from Widget base class and extends it with checkbox-specific features.
26
26
  """
27
27
 
28
+ SET_ATTR_TRIGGER_HOOK_MAP = {
29
+ "size": "update_check_radio_size",
30
+ }
31
+
28
32
  BUTTON_STYLE_MAPPING = {
29
33
  "check": "4",
30
34
  "cross": "5",
@@ -27,6 +27,11 @@ class Text(Widget):
27
27
  Inherits from Widget base class and extends it with text-specific features.
28
28
  """
29
29
 
30
+ SET_ATTR_TRIGGER_HOOK_MAP = {
31
+ "font_size": "update_text_field_font_size",
32
+ "font_color": "update_text_field_font_color",
33
+ }
34
+
30
35
  def __init__(
31
36
  self,
32
37
  name: str,
@@ -19,9 +19,9 @@ from pypdf.generic import (ArrayObject, DictionaryObject, NameObject,
19
19
  NumberObject, TextStringObject)
20
20
 
21
21
  from .constants import (AP, AS, BC, BG, BS, CA, DA, DV, FT,
22
- IMAGE_FIELD_IDENTIFIER, JS, MK, MULTILINE, READ_ONLY,
23
- TU, A, Btn, Ch, D, Ff, I, N, Off, Opt, Parent, Q, S,
24
- Sig, T, Tx, V, W, Yes)
22
+ IMAGE_FIELD_IDENTIFIER, JS, MK, READ_ONLY, TU, A, Btn,
23
+ Ch, D, Ff, I, N, Off, Opt, Parent, Q, S, Sig, T, Tx, V,
24
+ W, Yes)
25
25
  from .middleware.checkbox import Checkbox
26
26
  from .middleware.dropdown import Dropdown
27
27
  from .middleware.image import Image
@@ -273,38 +273,3 @@ def update_annotation_name(annot: DictionaryObject, val: str) -> None:
273
273
  annot[NameObject(Parent)][NameObject(T)] = TextStringObject(val)
274
274
  else:
275
275
  annot[NameObject(T)] = TextStringObject(val)
276
-
277
-
278
- def update_created_text_field_alignment(annot: DictionaryObject, val: int) -> None:
279
- """Update text alignment for created text field annotations.
280
-
281
- Modifies the alignment (Q) field of a text field annotation created
282
- by the library to set the specified text alignment.
283
-
284
- Args:
285
- annot: PDF text field annotation dictionary to modify
286
- val: Alignment value to set (typically 0=left, 1=center, 2=right)
287
- """
288
-
289
- annot[NameObject(Q)] = NumberObject(val)
290
-
291
-
292
- def update_created_text_field_multiline(annot: DictionaryObject, val: bool) -> None:
293
- """Update multiline flag for created text field annotations.
294
-
295
- Modifies the field flags (Ff) of a text field annotation created by
296
- the library to set or clear the multiline flag based on the input value.
297
-
298
- Args:
299
- annot: PDF text field annotation dictionary to modify
300
- val: Whether to enable multiline (True) or disable (False)
301
- """
302
-
303
- if val:
304
- annot[NameObject(Ff)] = NumberObject(int(annot[NameObject(Ff)]) | MULTILINE)
305
-
306
-
307
- NON_ACRO_FORM_PARAM_TO_FUNC = {
308
- ("TextWidget", "alignment"): update_created_text_field_alignment,
309
- ("TextWidget", "multiline"): update_created_text_field_multiline,
310
- }
@@ -16,7 +16,7 @@ from reportlab.lib.colors import Color
16
16
  from reportlab.pdfgen.canvas import Canvas
17
17
 
18
18
  from ..constants import Annots
19
- from ..patterns import NON_ACRO_FORM_PARAM_TO_FUNC
19
+ from ..hooks import NON_ACRO_FORM_PARAM_TO_FUNC
20
20
  from ..template import get_widget_key
21
21
  from ..utils import stream_to_io
22
22
 
@@ -36,6 +36,6 @@ class TextWidget(Widget):
36
36
  ("max_length", "maxlen"),
37
37
  ]
38
38
  COLOR_PARAMS = ["font_color", "bg_color", "border_color"]
39
- ALLOWED_NON_ACRO_FORM_PARAMS = ["alignment", "multiline"]
39
+ ALLOWED_NON_ACRO_FORM_PARAMS = ["alignment", "multiline", "comb"]
40
40
  NONE_DEFAULTS = ["max_length"]
41
41
  ACRO_FORM_FUNC = "textfield"