PyPDFForm 3.0.1__tar.gz → 3.1.1__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 (51) hide show
  1. {pypdfform-3.0.1 → pypdfform-3.1.1}/PKG-INFO +1 -1
  2. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/constants.py +3 -0
  4. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/filler.py +6 -4
  5. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/hooks.py +75 -4
  6. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/middleware/base.py +4 -1
  7. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/middleware/checkbox.py +5 -4
  8. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/middleware/dropdown.py +6 -5
  9. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/middleware/radio.py +5 -0
  10. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/middleware/text.py +10 -9
  11. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/patterns.py +88 -34
  12. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/template.py +12 -2
  13. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/utils.py +5 -1
  14. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm.egg-info/PKG-INFO +1 -1
  15. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm.egg-info/SOURCES.txt +1 -0
  16. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_create_widget.py +28 -0
  17. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_dropdown.py +80 -0
  18. pypdfform-3.1.1/tests/test_extract_values.py +72 -0
  19. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_functional.py +74 -0
  20. {pypdfform-3.0.1 → pypdfform-3.1.1}/LICENSE +0 -0
  21. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/adapter.py +0 -0
  22. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/coordinate.py +0 -0
  23. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/font.py +0 -0
  24. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/image.py +0 -0
  25. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/middleware/__init__.py +0 -0
  26. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/middleware/image.py +0 -0
  27. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/middleware/signature.py +0 -0
  28. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/watermark.py +0 -0
  29. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/__init__.py +0 -0
  30. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/base.py +0 -0
  31. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/bedrock.py +0 -0
  32. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/checkbox.py +0 -0
  33. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/dropdown.py +0 -0
  34. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/image.py +0 -0
  35. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/radio.py +0 -0
  36. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/signature.py +0 -0
  37. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/widgets/text.py +0 -0
  38. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm/wrapper.py +0 -0
  39. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  40. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm.egg-info/requires.txt +0 -0
  41. {pypdfform-3.0.1 → pypdfform-3.1.1}/PyPDFForm.egg-info/top_level.txt +0 -0
  42. {pypdfform-3.0.1 → pypdfform-3.1.1}/README.md +0 -0
  43. {pypdfform-3.0.1 → pypdfform-3.1.1}/pyproject.toml +0 -0
  44. {pypdfform-3.0.1 → pypdfform-3.1.1}/setup.cfg +0 -0
  45. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_adobe_mode.py +0 -0
  46. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_fill_max_length_text_field.py +0 -0
  47. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_fill_method.py +0 -0
  48. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_paragraph.py +0 -0
  49. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_signature.py +0 -0
  50. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_use_full_widget_name.py +0 -0
  51. {pypdfform-3.0.1 → pypdfform-3.1.1}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 3.0.1
3
+ Version: 3.1.1
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -20,7 +20,7 @@ The library supports various PDF form features, including:
20
20
  PyPDFForm aims to simplify PDF form manipulation, making it accessible to developers of all skill levels.
21
21
  """
22
22
 
23
- __version__ = "3.0.1"
23
+ __version__ = "3.1.1"
24
24
 
25
25
  from .middleware.text import Text # exposing for setting global font attrs
26
26
  from .wrapper import PdfWrapper
@@ -67,6 +67,7 @@ Opt = "/Opt"
67
67
  AS = "/AS"
68
68
  Yes = "/Yes"
69
69
  Off = "/Off"
70
+ XObject = "/XObject"
70
71
 
71
72
  # Font dict
72
73
  Length1 = "/Length1"
@@ -105,3 +106,5 @@ IMAGE_FIELD_IDENTIFIER = "event.target.buttonImportIcon();"
105
106
 
106
107
  COORDINATE_GRID_FONT_SIZE_MARGIN_RATIO = DEFAULT_FONT_SIZE / 100
107
108
  UNIQUE_SUFFIX_LENGTH = 20
109
+
110
+ SLASH = "/"
@@ -15,6 +15,7 @@ from pypdf import PdfReader, PdfWriter
15
15
  from pypdf.generic import DictionaryObject
16
16
 
17
17
  from .constants import WIDGET_TYPES, Annots
18
+ from .hooks import flatten_generic, flatten_radio
18
19
  from .image import get_draw_image_resolutions, get_image_dimensions
19
20
  from .middleware.checkbox import Checkbox
20
21
  from .middleware.dropdown import Dropdown
@@ -22,9 +23,8 @@ from .middleware.image import Image
22
23
  from .middleware.radio import Radio
23
24
  from .middleware.signature import Signature
24
25
  from .middleware.text import Text
25
- from .patterns import (flatten_generic, flatten_radio, update_checkbox_value,
26
- update_dropdown_value, update_radio_value,
27
- update_text_value)
26
+ from .patterns import (update_checkbox_value, update_dropdown_value,
27
+ update_radio_value, update_text_value)
28
28
  from .template import get_widget_key
29
29
  from .utils import stream_to_io
30
30
  from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
@@ -145,7 +145,9 @@ def fill(
145
145
 
146
146
  # flatten all
147
147
  if flatten:
148
- (flatten_radio if isinstance(widget, Radio) else flatten_generic)(annot)
148
+ (flatten_radio if isinstance(widget, Radio) else flatten_generic)(
149
+ annot, True
150
+ )
149
151
  if widget.value is None:
150
152
  continue
151
153
 
@@ -4,7 +4,8 @@ This module defines widget hooks that allow for dynamic modification of PDF form
4
4
 
5
5
  It provides functions to trigger these hooks, enabling changes to text field properties
6
6
  like font, font size, color, alignment, and multiline settings, as well as the size
7
- of checkbox and radio button widgets. These hooks are triggered during the PDF form
7
+ of checkbox and radio button widgets. It also provides functions for flattening
8
+ generic and radio button widgets. These hooks are triggered during the PDF form
8
9
  filling process, allowing for customization of the form's appearance and behavior.
9
10
  """
10
11
 
@@ -17,7 +18,7 @@ from pypdf.generic import (ArrayObject, DictionaryObject, FloatObject,
17
18
  NameObject, NumberObject, TextStringObject)
18
19
 
19
20
  from .constants import (COMB, DA, FONT_COLOR_IDENTIFIER, FONT_SIZE_IDENTIFIER,
20
- MULTILINE, Annots, Ff, Opt, Parent, Q, Rect)
21
+ MULTILINE, READ_ONLY, Annots, Ff, Opt, Parent, Q, Rect)
21
22
  from .template import get_widget_key
22
23
  from .utils import stream_to_io
23
24
 
@@ -249,8 +250,78 @@ def update_dropdown_choices(annot: DictionaryObject, val: list) -> None:
249
250
 
250
251
  Args:
251
252
  annot (DictionaryObject): The annotation dictionary for the dropdown field.
252
- val (list): A list of strings representing the new choices for the dropdown.
253
+ val (list): A list of strings or tuples representing the new choices for the dropdown.
253
254
  """
254
255
  annot[NameObject(Opt)] = ArrayObject(
255
- [ArrayObject([TextStringObject(each), TextStringObject(each)]) for each in val]
256
+ [
257
+ (
258
+ ArrayObject([TextStringObject(each[1]), TextStringObject(each[0])])
259
+ if isinstance(each, tuple)
260
+ else ArrayObject([TextStringObject(each), TextStringObject(each)])
261
+ )
262
+ for each in val
263
+ ]
256
264
  )
265
+
266
+
267
+ def flatten_radio(annot: DictionaryObject, val: bool) -> None:
268
+ """
269
+ Flattens a radio button annotation by setting or unsetting the ReadOnly flag,
270
+ making it non-editable or editable based on the `val` parameter.
271
+
272
+ This function modifies the Ff (flags) entry in the radio button's annotation
273
+ dictionary or its parent dictionary if `Parent` exists in `annot`, to set or
274
+ unset the ReadOnly flag, preventing or allowing the user from changing the
275
+ selected option.
276
+
277
+ Args:
278
+ annot (DictionaryObject): The radio button annotation dictionary.
279
+ val (bool): True to flatten (make read-only), False to unflatten (make editable).
280
+ """
281
+ if Parent in annot:
282
+ annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
283
+ (
284
+ int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) | READ_ONLY
285
+ if val
286
+ else int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) & ~READ_ONLY
287
+ )
288
+ )
289
+ else:
290
+ annot[NameObject(Ff)] = NumberObject(
291
+ (
292
+ int(annot.get(NameObject(Ff), 0)) | READ_ONLY
293
+ if val
294
+ else int(annot.get(NameObject(Ff), 0)) & ~READ_ONLY
295
+ )
296
+ )
297
+
298
+
299
+ def flatten_generic(annot: DictionaryObject, val: bool) -> None:
300
+ """
301
+ Flattens a generic annotation by setting or unsetting the ReadOnly flag,
302
+ making it non-editable or editable based on the `val` parameter.
303
+
304
+ This function modifies the Ff (flags) entry in the annotation dictionary to
305
+ set or unset the ReadOnly flag, preventing or allowing the user from
306
+ interacting with the form field.
307
+
308
+ Args:
309
+ annot (DictionaryObject): The annotation dictionary.
310
+ val (bool): True to flatten (make read-only), False to unflatten (make editable).
311
+ """
312
+ if Parent in annot and Ff not in annot:
313
+ annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
314
+ (
315
+ int(annot.get(NameObject(Ff), 0)) | READ_ONLY
316
+ if val
317
+ else int(annot.get(NameObject(Ff), 0)) & ~READ_ONLY
318
+ )
319
+ )
320
+ else:
321
+ annot[NameObject(Ff)] = NumberObject(
322
+ (
323
+ int(annot.get(NameObject(Ff), 0)) | READ_ONLY
324
+ if val
325
+ else int(annot.get(NameObject(Ff), 0)) & ~READ_ONLY
326
+ )
327
+ )
@@ -21,7 +21,9 @@ class Widget:
21
21
  as name, value, and schema definition.
22
22
  """
23
23
 
24
- SET_ATTR_TRIGGER_HOOK_MAP = {}
24
+ SET_ATTR_TRIGGER_HOOK_MAP = {
25
+ "readonly": "flatten_generic",
26
+ }
25
27
 
26
28
  def __init__(
27
29
  self,
@@ -39,6 +41,7 @@ class Widget:
39
41
  self._name = name
40
42
  self._value = value
41
43
  self.desc = None
44
+ self.readonly = None
42
45
  self.hooks_to_trigger = []
43
46
 
44
47
  def __setattr__(self, name: str, value: Any) -> None:
@@ -20,10 +20,6 @@ class Checkbox(Widget):
20
20
  implements the schema_definition and sample_value properties.
21
21
  """
22
22
 
23
- SET_ATTR_TRIGGER_HOOK_MAP = {
24
- "size": "update_check_radio_size",
25
- }
26
-
27
23
  def __init__(
28
24
  self,
29
25
  name: str,
@@ -39,6 +35,11 @@ class Checkbox(Widget):
39
35
  Attributes:
40
36
  size (int): The size of the checkbox. Defaults to None.
41
37
  """
38
+ self.SET_ATTR_TRIGGER_HOOK_MAP.update(
39
+ {
40
+ "size": "update_check_radio_size",
41
+ }
42
+ )
42
43
  super().__init__(name, value)
43
44
 
44
45
  self.size = None
@@ -26,11 +26,6 @@ class Dropdown(Widget):
26
26
  sample_value: Returns a sample value for the dropdown.
27
27
  """
28
28
 
29
- SET_ATTR_TRIGGER_HOOK_MAP = {
30
- "font": "update_text_field_font",
31
- "choices": "update_dropdown_choices",
32
- }
33
-
34
29
  def __init__(
35
30
  self,
36
31
  name: str,
@@ -47,6 +42,12 @@ class Dropdown(Widget):
47
42
  font (str): The font of the dropdown field.
48
43
  choices (List[str]): The list of choices for the dropdown.
49
44
  """
45
+ self.SET_ATTR_TRIGGER_HOOK_MAP.update(
46
+ {
47
+ "font": "update_text_field_font",
48
+ "choices": "update_dropdown_choices",
49
+ }
50
+ )
50
51
  super().__init__(name, value)
51
52
 
52
53
  self.font = None
@@ -34,6 +34,11 @@ class Radio(Checkbox):
34
34
  Attributes:
35
35
  number_of_options (int): The number of options for the radio button.
36
36
  """
37
+ self.SET_ATTR_TRIGGER_HOOK_MAP.update(
38
+ {
39
+ "readonly": "flatten_radio",
40
+ }
41
+ )
37
42
  super().__init__(name, value)
38
43
 
39
44
  self.number_of_options = 0
@@ -22,15 +22,6 @@ class Text(Widget):
22
22
  font_size, font_color, comb, alignment, and multiline.
23
23
  """
24
24
 
25
- SET_ATTR_TRIGGER_HOOK_MAP = {
26
- "font": "update_text_field_font",
27
- "font_size": "update_text_field_font_size",
28
- "font_color": "update_text_field_font_color",
29
- "comb": "update_text_field_comb",
30
- "alignment": "update_text_field_alignment",
31
- "multiline": "update_text_field_multiline",
32
- }
33
-
34
25
  def __init__(
35
26
  self,
36
27
  name: str,
@@ -52,6 +43,16 @@ class Text(Widget):
52
43
  multiline (bool): Whether the text field is multiline. Defaults to None.
53
44
  max_length (int): The maximum length of the text field. Defaults to None.
54
45
  """
46
+ self.SET_ATTR_TRIGGER_HOOK_MAP.update(
47
+ {
48
+ "font": "update_text_field_font",
49
+ "font_size": "update_text_field_font_size",
50
+ "font_color": "update_text_field_font_color",
51
+ "comb": "update_text_field_comb",
52
+ "alignment": "update_text_field_alignment",
53
+ "multiline": "update_text_field_multiline",
54
+ }
55
+ )
55
56
  super().__init__(name, value)
56
57
 
57
58
  self.font = None
@@ -5,15 +5,17 @@ This module defines patterns and utility functions for interacting with PDF form
5
5
  It includes patterns for identifying different types of widgets (e.g., text fields,
6
6
  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
- for updating and flattening these widgets.
8
+ for updating these widgets.
9
9
  """
10
10
 
11
+ from typing import Union
12
+
11
13
  from pypdf.generic import (ArrayObject, DictionaryObject, NameObject,
12
14
  NumberObject, TextStringObject)
13
15
 
14
- from .constants import (AP, AS, DV, FT, IMAGE_FIELD_IDENTIFIER, JS, READ_ONLY,
15
- TU, A, Btn, Ch, Ff, I, N, Off, Opt, Parent, Sig, T, Tx,
16
- V, Yes)
16
+ from .constants import (AP, AS, DV, FT, IMAGE_FIELD_IDENTIFIER, JS, SLASH, TU,
17
+ A, Btn, Ch, I, N, Off, Opt, Parent, Resources, Sig, T,
18
+ Tx, V, XObject, Yes)
17
19
  from .middleware.checkbox import Checkbox
18
20
  from .middleware.dropdown import Dropdown
19
21
  from .middleware.image import Image
@@ -22,6 +24,14 @@ from .middleware.signature import Signature
22
24
  from .middleware.text import Text
23
25
 
24
26
  WIDGET_TYPE_PATTERNS = [
27
+ # barcode? see pdf_samples/scenario/issues/1087.pdf
28
+ (
29
+ (
30
+ {FT: Tx},
31
+ {AP: {N: {Resources: {XObject: True}}}},
32
+ ),
33
+ None,
34
+ ),
25
35
  (
26
36
  ({A: {JS: IMAGE_FIELD_IDENTIFIER}},),
27
37
  Image,
@@ -43,7 +53,7 @@ WIDGET_TYPE_PATTERNS = [
43
53
  (
44
54
  {FT: Btn},
45
55
  {Parent: {FT: Btn}},
46
- {AS: (Yes, Off)},
56
+ {AS: (Yes, Off, SLASH)},
47
57
  ),
48
58
  Radio,
49
59
  ),
@@ -77,7 +87,7 @@ WIDGET_TYPE_PATTERNS = [
77
87
  (
78
88
  (
79
89
  {Parent: {FT: Btn}},
80
- {AS: (Yes, Off)},
90
+ {AS: (Yes, Off, SLASH)},
81
91
  ),
82
92
  Radio,
83
93
  ),
@@ -114,6 +124,23 @@ def update_checkbox_value(annot: DictionaryObject, check: bool = False) -> None:
114
124
  break
115
125
 
116
126
 
127
+ def get_checkbox_value(annot: DictionaryObject) -> Union[bool, None]:
128
+ """
129
+ Retrieves the boolean value of a checkbox annotation.
130
+
131
+ This function checks the value (V) of the checkbox annotation. If the value
132
+ is not 'Off', it means the checkbox is checked, and True is returned.
133
+ Otherwise, if the value is 'Off' or not present, None is returned.
134
+
135
+ Args:
136
+ annot (DictionaryObject): The checkbox annotation dictionary.
137
+
138
+ Returns:
139
+ Union[bool, None]: True if the checkbox is checked, None otherwise.
140
+ """
141
+ return True if annot.get(V, Off) != Off else None
142
+
143
+
117
144
  def update_radio_value(annot: DictionaryObject) -> None:
118
145
  """
119
146
  Updates the value of a radio button annotation, selecting it.
@@ -134,6 +161,28 @@ def update_radio_value(annot: DictionaryObject) -> None:
134
161
  break
135
162
 
136
163
 
164
+ def get_radio_value(annot: DictionaryObject) -> bool:
165
+ """
166
+ Retrieves the boolean value of a radio button annotation.
167
+
168
+ This function iterates through the appearance states (AP) of the radio button
169
+ annotation. If the value (V) of the parent dictionary matches any of these
170
+ appearance states, it means the radio button is selected, and True is returned.
171
+ Otherwise, False is returned.
172
+
173
+ Args:
174
+ annot (DictionaryObject): The radio button annotation dictionary.
175
+
176
+ Returns:
177
+ bool: True if the radio button is selected, False otherwise.
178
+ """
179
+ for each in annot.get(AP, {}).get(N, []):
180
+ if annot.get(Parent, {}).get(V) == each:
181
+ return True
182
+
183
+ return False
184
+
185
+
137
186
  def update_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
138
187
  """
139
188
  Updates the value of a dropdown annotation, selecting an option from the list.
@@ -158,6 +207,29 @@ def update_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
158
207
  annot[NameObject(I)] = ArrayObject([NumberObject(widget.value)])
159
208
 
160
209
 
210
+ def get_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
211
+ """
212
+ Retrieves the selected value of a dropdown annotation and updates the widget.
213
+
214
+ This function determines the current value of the dropdown, considering
215
+ whether it's a child annotation or a top-level one. It then iterates
216
+ through the widget's choices to find a match and sets the widget's
217
+ value to the index of the matched choice.
218
+
219
+ Args:
220
+ annot (DictionaryObject): The dropdown annotation dictionary.
221
+ widget (Dropdown): The Dropdown widget object to update with the retrieved value.
222
+ """
223
+ if Parent in annot and T not in annot:
224
+ to_compare = annot.get(Parent, {}).get(V)
225
+ else:
226
+ to_compare = annot.get(V)
227
+
228
+ for i, each in enumerate(widget.choices):
229
+ if each == to_compare:
230
+ widget.value = i or None # set None when 0
231
+
232
+
161
233
  def update_text_value(annot: DictionaryObject, widget: Text) -> None:
162
234
  """
163
235
  Updates the value of a text annotation, setting the text content.
@@ -177,40 +249,22 @@ def update_text_value(annot: DictionaryObject, widget: Text) -> None:
177
249
  annot[NameObject(AP)] = TextStringObject(widget.value)
178
250
 
179
251
 
180
- def flatten_radio(annot: DictionaryObject) -> None:
252
+ def get_text_value(annot: DictionaryObject, widget: Text) -> None:
181
253
  """
182
- Flattens a radio button annotation by setting the ReadOnly flag, making it non-editable.
254
+ Retrieves the text value of a text annotation and updates the widget.
183
255
 
184
- This function modifies the Ff (flags) entry in the radio button's parent
185
- dictionary to set the ReadOnly flag, preventing the user from changing the
186
- selected option.
256
+ This function determines the current text value of the annotation, considering
257
+ whether it's a child annotation or a top-level one, and then sets the
258
+ widget's value accordingly.
187
259
 
188
260
  Args:
189
- annot (DictionaryObject): The radio button annotation dictionary.
190
- """
191
- annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
192
- int(annot[NameObject(Parent)].get(NameObject(Ff), 0)) | READ_ONLY
193
- )
194
-
195
-
196
- def flatten_generic(annot: DictionaryObject) -> None:
197
- """
198
- Flattens a generic annotation by setting the ReadOnly flag, making it non-editable.
199
-
200
- This function modifies the Ff (flags) entry in the annotation dictionary to
201
- set the ReadOnly flag, preventing the user from interacting with the form field.
202
-
203
- Args:
204
- annot (DictionaryObject): The annotation dictionary.
261
+ annot (DictionaryObject): The text annotation dictionary.
262
+ widget (Text): The Text widget object to update with the retrieved value.
205
263
  """
206
- if Parent in annot and Ff not in annot:
207
- annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
208
- int(annot.get(NameObject(Ff), 0)) | READ_ONLY
209
- )
264
+ if Parent in annot and T not in annot:
265
+ widget.value = annot[Parent].get(V)
210
266
  else:
211
- annot[NameObject(Ff)] = NumberObject(
212
- int(annot.get(NameObject(Ff), 0)) | READ_ONLY
213
- )
267
+ widget.value = annot.get(V)
214
268
 
215
269
 
216
270
  def update_annotation_name(annot: DictionaryObject, val: str) -> None:
@@ -16,12 +16,14 @@ from pypdf import PdfReader, PdfWriter
16
16
  from pypdf.generic import DictionaryObject
17
17
 
18
18
  from .constants import WIDGET_TYPES, Annots, MaxLen, Parent, T
19
+ from .middleware.checkbox import Checkbox
19
20
  from .middleware.dropdown import Dropdown
20
21
  from .middleware.radio import Radio
21
22
  from .middleware.text import Text
22
23
  from .patterns import (DROPDOWN_CHOICE_PATTERNS, WIDGET_DESCRIPTION_PATTERNS,
23
24
  WIDGET_KEY_PATTERNS, WIDGET_TYPE_PATTERNS,
24
- update_annotation_name)
25
+ get_checkbox_value, get_dropdown_value, get_radio_value,
26
+ get_text_value, update_annotation_name)
25
27
  from .utils import extract_widget_property, find_pattern_match, stream_to_io
26
28
 
27
29
 
@@ -61,11 +63,16 @@ def build_widgets(
61
63
  if isinstance(_widget, Text):
62
64
  # mostly for schema for now
63
65
  _widget.max_length = get_text_field_max_length(widget)
66
+ get_text_value(widget, _widget)
67
+
68
+ if type(_widget) is Checkbox:
69
+ _widget.value = get_checkbox_value(widget)
64
70
 
65
71
  if isinstance(_widget, Dropdown):
66
72
  # actually used for filling value
67
73
  # doesn't trigger hook
68
74
  _widget.__dict__["choices"] = get_dropdown_choices(widget)
75
+ get_dropdown_value(widget, _widget)
69
76
 
70
77
  if isinstance(_widget, Radio):
71
78
  if key not in results:
@@ -73,6 +80,9 @@ def build_widgets(
73
80
 
74
81
  # for schema
75
82
  results[key].number_of_options += 1
83
+
84
+ if get_radio_value(widget):
85
+ results[key].value = results[key].number_of_options - 1
76
86
  continue
77
87
 
78
88
  results[key] = _widget
@@ -172,7 +182,7 @@ def construct_widget(widget: dict, key: str) -> Union[WIDGET_TYPES, None]:
172
182
  for pattern in patterns:
173
183
  check = check and find_pattern_match(pattern, widget)
174
184
  if check:
175
- result = _type(key)
185
+ result = _type(key) if _type else None
176
186
  break
177
187
  return result
178
188
 
@@ -23,7 +23,7 @@ from typing import Any, BinaryIO, List, Union
23
23
  from pypdf import PdfReader, PdfWriter
24
24
  from pypdf.generic import ArrayObject, DictionaryObject, NameObject
25
25
 
26
- from .constants import UNIQUE_SUFFIX_LENGTH, XFA, AcroForm, Annots, Root
26
+ from .constants import SLASH, UNIQUE_SUFFIX_LENGTH, XFA, AcroForm, Annots, Root
27
27
 
28
28
 
29
29
  @lru_cache
@@ -220,6 +220,10 @@ def find_pattern_match(pattern: dict, widget: Union[dict, DictionaryObject]) ->
220
220
  else:
221
221
  if isinstance(pattern[key], tuple):
222
222
  result = value in pattern[key]
223
+ if not result and SLASH in pattern[key] and value.startswith(SLASH):
224
+ result = True
225
+ elif pattern[key] is True:
226
+ result = True
223
227
  else:
224
228
  result = pattern[key] == value
225
229
  if result:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 3.0.1
3
+ Version: 3.1.1
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -39,6 +39,7 @@ PyPDFForm/widgets/text.py
39
39
  tests/test_adobe_mode.py
40
40
  tests/test_create_widget.py
41
41
  tests/test_dropdown.py
42
+ tests/test_extract_values.py
42
43
  tests/test_fill_max_length_text_field.py
43
44
  tests/test_fill_method.py
44
45
  tests/test_functional.py
@@ -832,6 +832,34 @@ def test_create_dropdown(template_stream, pdf_samples, sample_font_stream, reque
832
832
  assert obj.read() == expected
833
833
 
834
834
 
835
+ def test_create_dropdown_with_export_values(template_stream, pdf_samples, request):
836
+ expected_path = os.path.join(
837
+ pdf_samples, "widget", "test_create_dropdown_with_export_values.pdf"
838
+ )
839
+ with open(expected_path, "rb+") as f:
840
+ obj = PdfWrapper(template_stream).create_widget(
841
+ widget_type="dropdown",
842
+ name="new_dropdown_widget",
843
+ page_number=1,
844
+ x=57,
845
+ y=700,
846
+ options=[
847
+ ("foo", "foo_export"),
848
+ ("bar", "bar_export"),
849
+ ("foobar", "foobar_export"),
850
+ ],
851
+ )
852
+ assert obj.schema["properties"]["new_dropdown_widget"]["type"] == "integer"
853
+
854
+ request.config.results["expected_path"] = expected_path
855
+ request.config.results["stream"] = obj.read()
856
+
857
+ expected = f.read()
858
+
859
+ assert len(obj.read()) == len(expected)
860
+ assert obj.read() == expected
861
+
862
+
835
863
  def test_fill_cmyk_color(pdf_samples, request):
836
864
  expected_path = os.path.join(pdf_samples, "widget", "test_fill_cmyk_color.pdf")
837
865
  with open(expected_path, "rb+") as f:
@@ -290,6 +290,33 @@ def test_dropdown_alignment_flatten(dropdown_alignment, pdf_samples, request):
290
290
  assert obj.read() == expected
291
291
 
292
292
 
293
+ def test_dropdown_alignment_flatten_then_unflatten(
294
+ dropdown_alignment, pdf_samples, request
295
+ ):
296
+ expected_path = os.path.join(
297
+ pdf_samples, "dropdown", "test_dropdown_alignment_flatten_then_unflatten.pdf"
298
+ )
299
+ with open(expected_path, "rb+") as f:
300
+ obj = PdfWrapper(dropdown_alignment).fill(
301
+ {
302
+ "dropdown_left": 0,
303
+ "dropdown_center": 1,
304
+ "dropdown_right": 2,
305
+ },
306
+ flatten=True,
307
+ )
308
+ obj.widgets["dropdown_center"].readonly = False
309
+
310
+ request.config.results["expected_path"] = expected_path
311
+ request.config.results["stream"] = obj.read()
312
+
313
+ expected = f.read()
314
+
315
+ if os.name != "nt":
316
+ assert len(obj.read()) == len(expected)
317
+ assert obj.read() == expected
318
+
319
+
293
320
  def test_dropdown_alignment_sejda(dropdown_alignment_sejda, pdf_samples, request):
294
321
  expected_path = os.path.join(
295
322
  pdf_samples, "dropdown", "test_dropdown_alignment_sejda.pdf"
@@ -343,6 +370,37 @@ def test_dropdown_alignment_sejda_flatten(
343
370
  assert obj.read() == expected
344
371
 
345
372
 
373
+ def test_dropdown_alignment_sejda_flatten_then_unflatten(
374
+ dropdown_alignment_sejda, pdf_samples, request
375
+ ):
376
+ expected_path = os.path.join(
377
+ pdf_samples,
378
+ "dropdown",
379
+ "test_dropdown_alignment_sejda_flatten_then_unflatten.pdf",
380
+ )
381
+ with open(
382
+ expected_path,
383
+ "rb+",
384
+ ) as f:
385
+ obj = PdfWrapper(dropdown_alignment_sejda).fill(
386
+ {
387
+ "dropdown_left": 0,
388
+ "dropdown_center": 1,
389
+ "dropdown_right": 2,
390
+ },
391
+ flatten=True,
392
+ )
393
+ obj.widgets["dropdown_center"].readonly = False
394
+
395
+ request.config.results["expected_path"] = expected_path
396
+ request.config.results["stream"] = obj.read()
397
+
398
+ expected = f.read()
399
+
400
+ assert len(obj.read()) == len(expected)
401
+ assert obj.read() == expected
402
+
403
+
346
404
  def test_change_dropdown_choices(sample_template_with_dropdown, pdf_samples, request):
347
405
  expected_path = os.path.join(
348
406
  pdf_samples, "dropdown", "test_change_dropdown_choices.pdf"
@@ -358,3 +416,25 @@ def test_change_dropdown_choices(sample_template_with_dropdown, pdf_samples, req
358
416
 
359
417
  assert len(obj.read()) == len(expected)
360
418
  assert obj.read() == expected
419
+
420
+
421
+ def test_change_dropdown_choices_with_export_values(
422
+ sample_template_with_dropdown, pdf_samples, request
423
+ ):
424
+ expected_path = os.path.join(
425
+ pdf_samples, "dropdown", "test_change_dropdown_choices_with_export_values.pdf"
426
+ )
427
+ with open(expected_path, "rb+") as f:
428
+ obj = PdfWrapper(sample_template_with_dropdown, adobe_mode=True)
429
+ obj.widgets["dropdown_1"].choices = [
430
+ ("apple", "apple_export"),
431
+ ("banana", "banana_export"),
432
+ ]
433
+
434
+ request.config.results["expected_path"] = expected_path
435
+ request.config.results["stream"] = obj.read()
436
+
437
+ expected = f.read()
438
+
439
+ assert len(obj.read()) == len(expected)
440
+ assert obj.read() == expected
@@ -0,0 +1,72 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+
5
+ from PyPDFForm import PdfWrapper
6
+
7
+
8
+ def test_text_check_values(pdf_samples, data_dict):
9
+ obj = PdfWrapper(os.path.join(pdf_samples, "test_fill.pdf"))
10
+
11
+ for k, v in data_dict.items():
12
+ if v is False:
13
+ assert obj.widgets[k].value is None
14
+ else:
15
+ assert obj.widgets[k].value == v
16
+
17
+
18
+ def test_radio_values(pdf_samples):
19
+ data_dict = {
20
+ "radio_1": 0,
21
+ "radio_2": 1,
22
+ "radio_3": 2,
23
+ }
24
+ obj = PdfWrapper(os.path.join(pdf_samples, "test_fill_radiobutton.pdf"))
25
+
26
+ for k, v in data_dict.items():
27
+ assert obj.widgets[k].value == v
28
+
29
+
30
+ def test_sejda_values(pdf_samples, sejda_data):
31
+ obj = PdfWrapper(os.path.join(pdf_samples, "test_fill_sejda.pdf"))
32
+
33
+ for k, v in sejda_data.items():
34
+ if v is False:
35
+ assert obj.widgets[k].value is None
36
+ else:
37
+ assert obj.widgets[k].value == v
38
+
39
+
40
+ def test_dropdown_values(pdf_samples):
41
+ obj = PdfWrapper(os.path.join(pdf_samples, "dropdown", "test_dropdown_two.pdf"))
42
+
43
+ assert obj.widgets["dropdown_1"].value == 1
44
+
45
+
46
+ def test_dropdown_default_values(pdf_samples):
47
+ obj = PdfWrapper(os.path.join(pdf_samples, "dropdown", "test_dropdown_one.pdf"))
48
+
49
+ assert obj.widgets["dropdown_1"].value is None
50
+
51
+
52
+ def test_sejda_dropdown_values(pdf_samples):
53
+ obj = PdfWrapper(
54
+ os.path.join(pdf_samples, "dropdown", "test_dropdown_alignment_sejda.pdf")
55
+ )
56
+
57
+ assert obj.widgets["dropdown_left"].value is None
58
+ assert obj.widgets["dropdown_center"].value == 1
59
+ assert obj.widgets["dropdown_right"].value == 2
60
+
61
+
62
+ def test_addition_operator_3_times_values(template_stream, data_dict):
63
+ result = PdfWrapper()
64
+
65
+ for _ in range(3):
66
+ result += PdfWrapper(template_stream).fill(data_dict)
67
+
68
+ obj = PdfWrapper(result.read())
69
+
70
+ for k, v in obj.widgets.items():
71
+ if k.split("-")[0] in data_dict:
72
+ assert (v.value or False) == data_dict[k.split("-")[0]]
@@ -55,6 +55,24 @@ def test_fill_flatten(template_stream, pdf_samples, data_dict, request):
55
55
  assert obj.read() == expected
56
56
 
57
57
 
58
+ def test_fill_flatten_then_unflatten(template_stream, pdf_samples, data_dict, request):
59
+ expected_path = os.path.join(pdf_samples, "test_fill_flatten_then_unflatten.pdf")
60
+ with open(expected_path, "rb+") as f:
61
+ obj = PdfWrapper(template_stream).fill(data_dict, flatten=True)
62
+ obj.widgets["test_2"].readonly = False
63
+ obj.widgets["check_3"].readonly = False
64
+
65
+ request.config.results["expected_path"] = expected_path
66
+ request.config.results["stream"] = obj.read()
67
+ assert len(obj.read()) == len(obj.read())
68
+ assert obj.read() == obj.read()
69
+
70
+ expected = f.read()
71
+
72
+ assert len(obj.read()) == len(expected)
73
+ assert obj.read() == expected
74
+
75
+
58
76
  def test_register_bad_fonts():
59
77
  assert not PdfWrapper().register_font("foo", b"foo").read()
60
78
  assert not PdfWrapper().register_font("foo", "foo").read()
@@ -324,6 +342,35 @@ def test_fill_radiobutton_flatten(
324
342
  assert obj.read() == expected
325
343
 
326
344
 
345
+ def test_fill_radiobutton_flatten_then_unflatten(
346
+ template_with_radiobutton_stream, pdf_samples, request
347
+ ):
348
+ expected_path = os.path.join(
349
+ pdf_samples, "test_fill_radiobutton_flatten_then_unflatten.pdf"
350
+ )
351
+ with open(
352
+ expected_path,
353
+ "rb+",
354
+ ) as f:
355
+ obj = PdfWrapper(template_with_radiobutton_stream).fill(
356
+ {
357
+ "radio_1": 0,
358
+ "radio_2": 1,
359
+ "radio_3": 2,
360
+ },
361
+ flatten=True,
362
+ )
363
+ obj.widgets["radio_2"].readonly = False
364
+
365
+ request.config.results["expected_path"] = expected_path
366
+ request.config.results["stream"] = obj.read()
367
+
368
+ expected = f.read()
369
+
370
+ assert len(obj.read()) == len(expected)
371
+ assert obj.read() == expected
372
+
373
+
327
374
  def test_fill_sejda(sejda_template, pdf_samples, sejda_data, request):
328
375
  expected_path = os.path.join(pdf_samples, "test_fill_sejda.pdf")
329
376
  with open(
@@ -361,6 +408,33 @@ def test_fill_sejda_flatten(sejda_template, pdf_samples, sejda_data, request):
361
408
  assert obj.read() == expected
362
409
 
363
410
 
411
+ def test_fill_sejda_flatten_then_unflatten(
412
+ sejda_template, pdf_samples, sejda_data, request
413
+ ):
414
+ expected_path = os.path.join(
415
+ pdf_samples, "test_fill_sejda_flatten_then_unflatten.pdf"
416
+ )
417
+ with open(
418
+ expected_path,
419
+ "rb+",
420
+ ) as f:
421
+ obj = PdfWrapper(sejda_template).fill(
422
+ sejda_data,
423
+ flatten=True,
424
+ )
425
+ obj.widgets["buyer_name"].readonly = False
426
+ obj.widgets["at_future_date"].readonly = False
427
+ obj.widgets["purchase_option"].readonly = False
428
+
429
+ request.config.results["expected_path"] = expected_path
430
+ request.config.results["stream"] = obj.read()
431
+
432
+ expected = f.read()
433
+
434
+ assert len(obj.read()) == len(expected)
435
+ assert obj.read() == expected
436
+
437
+
364
438
  def test_draw_text_on_one_page(template_stream, pdf_samples, request):
365
439
  expected_path = os.path.join(pdf_samples, "test_draw_text_on_one_page.pdf")
366
440
  with open(expected_path, "rb+") as f:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes