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.
Files changed (44) hide show
  1. PyPDFForm/__init__.py +5 -3
  2. PyPDFForm/adapter.py +33 -1
  3. PyPDFForm/ap.py +99 -0
  4. PyPDFForm/assets/__init__.py +0 -0
  5. PyPDFForm/assets/blank.py +100 -0
  6. PyPDFForm/constants.py +20 -2
  7. PyPDFForm/coordinate.py +7 -11
  8. PyPDFForm/deprecation.py +30 -0
  9. PyPDFForm/filler.py +17 -36
  10. PyPDFForm/font.py +16 -16
  11. PyPDFForm/hooks.py +153 -30
  12. PyPDFForm/image.py +0 -3
  13. PyPDFForm/middleware/__init__.py +35 -0
  14. PyPDFForm/middleware/base.py +24 -5
  15. PyPDFForm/middleware/checkbox.py +18 -1
  16. PyPDFForm/middleware/signature.py +0 -1
  17. PyPDFForm/patterns.py +44 -13
  18. PyPDFForm/raw/__init__.py +37 -0
  19. PyPDFForm/raw/circle.py +65 -0
  20. PyPDFForm/raw/ellipse.py +69 -0
  21. PyPDFForm/raw/image.py +79 -0
  22. PyPDFForm/raw/line.py +65 -0
  23. PyPDFForm/raw/rect.py +70 -0
  24. PyPDFForm/raw/text.py +73 -0
  25. PyPDFForm/template.py +114 -12
  26. PyPDFForm/types.py +49 -0
  27. PyPDFForm/utils.py +31 -41
  28. PyPDFForm/watermark.py +153 -44
  29. PyPDFForm/widgets/__init__.py +1 -0
  30. PyPDFForm/widgets/base.py +79 -59
  31. PyPDFForm/widgets/checkbox.py +30 -30
  32. PyPDFForm/widgets/dropdown.py +42 -40
  33. PyPDFForm/widgets/image.py +17 -16
  34. PyPDFForm/widgets/radio.py +27 -28
  35. PyPDFForm/widgets/signature.py +96 -60
  36. PyPDFForm/widgets/text.py +40 -40
  37. PyPDFForm/wrapper.py +256 -240
  38. {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/METADATA +33 -26
  39. pypdfform-4.2.0.dist-info/RECORD +47 -0
  40. {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/licenses/LICENSE +1 -1
  41. pypdfform-3.5.3.dist-info/RECORD +0 -35
  42. /PyPDFForm/{widgets → assets}/bedrock.py +0 -0
  43. {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/WHEEL +0 -0
  44. {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 .constants import (COMB, DA, FONT_COLOR_IDENTIFIER, FONT_SIZE_IDENTIFIER,
25
- MULTILINE, READ_ONLY, REQUIRED, TU, Annots, Ff, MaxLen,
26
- Opt, Parent, Q, Rect)
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
- # TODO: investigate this more
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 not in annot:
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
- # TODO: add a test case when supporting edit required
416
- # if Parent in annot and Ff not in annot:
417
- # annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
418
- # (
419
- # int(annot.get(NameObject(Ff), 0)) | REQUIRED
420
- # if val
421
- # else int(annot.get(NameObject(Ff), 0)) & ~REQUIRED
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
@@ -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
@@ -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.desc: str = None
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.desc is not None:
111
- result["description"] = self.desc
129
+ if self.tooltip is not None:
130
+ result["description"] = self.tooltip
112
131
 
113
132
  return result
114
133
 
@@ -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, Sig,
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(annot: DictionaryObject, widget: Dropdown) -> None:
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
- annot[NameObject(AP)] = TextStringObject(choices[widget.value])
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(annot: DictionaryObject, widget: Text) -> None:
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
- annot[NameObject(AP)] = TextStringObject(widget.value)
248
+ if not need_appearances:
249
+ annot[NameObject(AP)] = TextStringObject(widget.value)
243
250
  else:
244
251
  annot[NameObject(V)] = TextStringObject(widget.value)
245
- annot[NameObject(AP)] = TextStringObject(widget.value)
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
@@ -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
+ }
@@ -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
+ }