PyPDFForm 3.5.1__py3-none-any.whl → 4.2.0__py3-none-any.whl

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