PyPDFForm 5.5.2__tar.gz → 5.5.3__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.
Files changed (88) hide show
  1. {pypdfform-5.5.2 → pypdfform-5.5.3}/PKG-INFO +1 -1
  2. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/constants.py +13 -1
  4. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/egress.py +2 -2
  5. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/patterns.py +2 -2
  6. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/base.py +24 -11
  7. pypdfform-5.5.3/PyPDFForm/lib/widgets/image.py +193 -0
  8. pypdfform-5.5.3/PyPDFForm/lib/widgets/signature.py +353 -0
  9. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/wrapper.py +6 -12
  10. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/PKG-INFO +1 -1
  11. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/SOURCES.txt +0 -1
  12. pypdfform-5.5.2/PyPDFForm/lib/assets/bedrock.py +0 -4
  13. pypdfform-5.5.2/PyPDFForm/lib/widgets/image.py +0 -50
  14. pypdfform-5.5.2/PyPDFForm/lib/widgets/signature.py +0 -201
  15. {pypdfform-5.5.2 → pypdfform-5.5.3}/LICENSE +0 -0
  16. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/__init__.py +0 -0
  17. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/common.py +0 -0
  18. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/create.py +0 -0
  19. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/entry.py +0 -0
  20. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/inspect.py +0 -0
  21. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/remove.py +0 -0
  22. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/root.py +0 -0
  23. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/schemas/__init__.py +0 -0
  24. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/schemas/create.py +0 -0
  25. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/schemas/update.py +0 -0
  26. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/update.py +0 -0
  27. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/__init__.py +0 -0
  28. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/adapter.py +0 -0
  29. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/__init__.py +0 -0
  30. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/base.py +0 -0
  31. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/link.py +0 -0
  32. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/stamp.py +0 -0
  33. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/text.py +0 -0
  34. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  35. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/assets/__init__.py +0 -0
  36. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/assets/blank.py +0 -0
  37. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/coordinate.py +0 -0
  38. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/deprecation.py +0 -0
  39. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/filler.py +0 -0
  40. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/font.py +0 -0
  41. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/hooks.py +0 -0
  42. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/image.py +0 -0
  43. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/__init__.py +0 -0
  44. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/base.py +0 -0
  45. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  46. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  47. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/image.py +0 -0
  48. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/radio.py +0 -0
  49. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/signature.py +0 -0
  50. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/text.py +0 -0
  51. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/__init__.py +0 -0
  52. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/circle.py +0 -0
  53. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/ellipse.py +0 -0
  54. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/image.py +0 -0
  55. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/line.py +0 -0
  56. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/rect.py +0 -0
  57. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/text.py +0 -0
  58. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/template.py +0 -0
  59. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/types.py +0 -0
  60. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/utils.py +0 -0
  61. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/watermark.py +0 -0
  62. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/__init__.py +0 -0
  63. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  64. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  65. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/radio.py +0 -0
  66. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/text.py +0 -0
  67. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  68. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/entry_points.txt +0 -0
  69. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/requires.txt +0 -0
  70. {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/top_level.txt +0 -0
  71. {pypdfform-5.5.2 → pypdfform-5.5.3}/README.md +0 -0
  72. {pypdfform-5.5.2 → pypdfform-5.5.3}/pyproject.toml +0 -0
  73. {pypdfform-5.5.2 → pypdfform-5.5.3}/setup.cfg +0 -0
  74. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_bulk_create_fields.py +0 -0
  75. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_create_widget.py +0 -0
  76. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_draw_elements.py +0 -0
  77. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_dropdown.py +0 -0
  78. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_extract_middleware_attributes.py +0 -0
  79. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_fill_max_length_text_field.py +0 -0
  80. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_font_widths.py +0 -0
  81. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_functional.py +0 -0
  82. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_generate_appearance_streams.py +0 -0
  83. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_js.py +0 -0
  84. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_need_appearances.py +0 -0
  85. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_paragraph.py +0 -0
  86. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_signature.py +0 -0
  87. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_use_full_widget_name.py +0 -0
  88. {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 5.5.2
3
+ Version: 5.5.3
4
4
  Summary: The Python library & CLI for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -18,7 +18,7 @@ from Python code or from the command line.
18
18
 
19
19
  import logging
20
20
 
21
- __version__ = "5.5.2"
21
+ __version__ = "5.5.3"
22
22
 
23
23
  from .lib.annotations import Annotations
24
24
  from .lib.assets.blank import BlankPage
@@ -47,6 +47,10 @@ Annots = "/Annots"
47
47
  Annot = "/Annot"
48
48
  Contents = "/Contents"
49
49
  Widget = "/Widget"
50
+ XObject = "/XObject"
51
+ Form = "/Form"
52
+ BBox = "/BBox"
53
+ Matrix = "/Matrix"
50
54
  T = "/T"
51
55
  TU = "/TU"
52
56
  Rect = "/Rect"
@@ -59,6 +63,7 @@ V = "/V"
59
63
  AP = "/AP"
60
64
  I = "/I" # noqa: E741
61
65
  N = "/N"
66
+ R = "/R"
62
67
  Sig = "/Sig"
63
68
  DA = "/DA"
64
69
  DR = "/DR"
@@ -71,6 +76,13 @@ Opt = "/Opt"
71
76
  AS = "/AS"
72
77
  Yes = "/Yes"
73
78
  Off = "/Off"
79
+ MK = "/MK"
80
+ BC = "/BC"
81
+ BS = "/BS"
82
+ W = "/W"
83
+ H = "/H"
84
+ TP = "/TP"
85
+ IF = "/IF"
74
86
 
75
87
  # javascript
76
88
  A = "/A"
@@ -144,7 +156,7 @@ DEFAULT_FONT = "Helvetica"
144
156
  DEFAULT_FONT_SIZE = 12
145
157
  DEFAULT_FONT_COLOR = (0, 0, 0)
146
158
 
147
- IMAGE_FIELD_IDENTIFIER = "event.target.buttonImportIcon();"
159
+ IMAGE_IMPORT_JAVASCRIPT = "event.target.buttonImportIcon();"
148
160
 
149
161
  COORDINATE_GRID_FONT_SIZE_MARGIN_RATIO = DEFAULT_FONT_SIZE / 100
150
162
  UNIQUE_SUFFIX_LENGTH = 20
@@ -119,8 +119,8 @@ def rebuild_acroform_fields(
119
119
  fields.append(field_ref)
120
120
  seen_fields.add(field_key)
121
121
 
122
- # if not seen_fields:
123
- # return pdf
122
+ if not seen_fields:
123
+ return pdf
124
124
 
125
125
  if AcroForm not in root:
126
126
  root[NameObject(AcroForm)] = DictionaryObject({})
@@ -24,7 +24,7 @@ from .constants import (
24
24
  DV,
25
25
  FT,
26
26
  HIDDEN,
27
- IMAGE_FIELD_IDENTIFIER,
27
+ IMAGE_IMPORT_JAVASCRIPT,
28
28
  JS,
29
29
  SLASH,
30
30
  TU,
@@ -61,7 +61,7 @@ WIDGET_TYPE_PATTERNS = [
61
61
  (
62
62
  (
63
63
  {Subtype: Widget},
64
- {A: {JS: IMAGE_FIELD_IDENTIFIER}},
64
+ {A: {JS: IMAGE_IMPORT_JAVASCRIPT}},
65
65
  ),
66
66
  Image,
67
67
  ),
@@ -18,7 +18,7 @@ from dataclasses import dataclass
18
18
  from functools import lru_cache
19
19
  from inspect import signature
20
20
  from io import BytesIO
21
- from typing import List, Optional
21
+ from typing import Any, List, Optional
22
22
 
23
23
  from pypdf import PdfReader
24
24
  from reportlab.lib.colors import Color
@@ -172,6 +172,27 @@ class Widget:
172
172
  """
173
173
  getattr(canvas.acroForm, self.ACRO_FORM_FUNC)(**self.acro_form_params)
174
174
 
175
+ @staticmethod
176
+ def create_watermark_canvas(page: Any) -> tuple[BytesIO, Canvas]:
177
+ """
178
+ Creates a PDF canvas sized to a source page.
179
+
180
+ Args:
181
+ page (Any): The source PDF page whose media box defines the canvas size.
182
+
183
+ Returns:
184
+ tuple[BytesIO, Canvas]: The in-memory stream and its PDF canvas.
185
+ """
186
+ watermark = BytesIO()
187
+ canvas = Canvas(
188
+ watermark,
189
+ pagesize=(
190
+ float(page.mediabox[2]),
191
+ float(page.mediabox[3]),
192
+ ),
193
+ )
194
+ return watermark, canvas
195
+
175
196
  @staticmethod
176
197
  def bulk_watermarks(widgets: List[Widget], stream: bytes) -> List[bytes]:
177
198
  """
@@ -209,18 +230,10 @@ class Widget:
209
230
  if page_num not in widgets_by_page:
210
231
  continue
211
232
 
233
+ page = pdf.pages[page_num - 1]
212
234
  # Use a fresh buffer per page to avoid stale trailing bytes
213
235
  # when the current page watermark is smaller than a previous page.
214
- watermark = BytesIO()
215
- page = pdf.pages[page_num - 1]
216
-
217
- canvas = Canvas(
218
- watermark,
219
- pagesize=(
220
- float(page.mediabox[2]),
221
- float(page.mediabox[3]),
222
- ),
223
- )
236
+ watermark, canvas = Widget.create_watermark_canvas(page)
224
237
 
225
238
  for widget in widgets_by_page[page_num]:
226
239
  getattr(widget, "_required_handler")(canvas)
@@ -0,0 +1,193 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module defines the `ImageField` and `ImageWidget` classes, which are used
4
+ to describe and construct image-import form fields.
5
+
6
+ `ImageField` inherits the signature field's sizing properties. `ImageWidget`
7
+ reuses the signature widget's placement and carrier-PDF infrastructure while
8
+ constructing a push-button annotation with an Acrobat JavaScript image-import
9
+ action.
10
+ """
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Any, List, Type
14
+
15
+ from pypdf import PdfWriter
16
+ from pypdf.generic import (
17
+ ArrayObject,
18
+ DictionaryObject,
19
+ FloatObject,
20
+ NameObject,
21
+ NumberObject,
22
+ TextStringObject,
23
+ )
24
+
25
+ from ..constants import (
26
+ AP,
27
+ BC,
28
+ BS,
29
+ DA,
30
+ FT,
31
+ IF,
32
+ IMAGE_IMPORT_JAVASCRIPT,
33
+ JS,
34
+ MK,
35
+ TP,
36
+ A,
37
+ Action,
38
+ Annot,
39
+ Btn,
40
+ D,
41
+ F,
42
+ Ff,
43
+ JavaScript,
44
+ Matrix,
45
+ N,
46
+ R,
47
+ Rect,
48
+ S,
49
+ Subtype,
50
+ T,
51
+ W,
52
+ Widget,
53
+ )
54
+ from ..constants import Type as PdfType
55
+ from .signature import SignatureField, SignatureWidget
56
+
57
+
58
+ class ImageWidget(SignatureWidget):
59
+ """
60
+ Represents an image widget in a PDF form.
61
+
62
+ The widget inherits signature-field placement, dimensions, and deferred
63
+ hooks, but constructs a push-button annotation identified by its
64
+ `buttonImportIcon()` JavaScript action.
65
+ """
66
+
67
+ @staticmethod
68
+ def _build_annotation(out: PdfWriter, widget: SignatureWidget) -> Any:
69
+ """
70
+ Constructs an image-import widget annotation owned by a PDF writer.
71
+
72
+ This method creates a border-only Form XObject, reuses it for the normal,
73
+ rollover, and pressed appearances, and builds a push-button `/Btn` widget
74
+ whose JavaScript action invokes `buttonImportIcon()`. It registers the
75
+ appearance and annotation with the same writer and implements the
76
+ annotation-builder callback used by `build_widget_watermarks`.
77
+
78
+ Args:
79
+ out (PdfWriter): The writer that will own the appearance stream and
80
+ widget annotation.
81
+ widget (SignatureWidget): The normalized image widget definition to
82
+ convert into a PDF annotation.
83
+
84
+ Returns:
85
+ Any: The writer-owned indirect reference to the widget annotation.
86
+ """
87
+ width = float(widget.optional_parameters["width"])
88
+ height = float(widget.optional_parameters["height"])
89
+ border_color = (0.1, 0.1, 0.1)
90
+
91
+ appearance_stream = ImageWidget._build_border_appearance(
92
+ width, height, border_color
93
+ )
94
+ appearance_stream[NameObject(Matrix)] = ArrayObject(
95
+ [
96
+ FloatObject(1),
97
+ FloatObject(0),
98
+ FloatObject(0),
99
+ FloatObject(1),
100
+ FloatObject(0),
101
+ FloatObject(0),
102
+ ]
103
+ )
104
+ appearance = out._add_object( # type: ignore # noqa: SLF001 # pylint: disable=W0212
105
+ appearance_stream.flate_encode()
106
+ )
107
+
108
+ annotation = DictionaryObject(
109
+ {
110
+ NameObject(FT): NameObject(Btn),
111
+ NameObject(Ff): NumberObject(1 << 16),
112
+ NameObject(PdfType): NameObject(Annot),
113
+ NameObject(Subtype): NameObject(Widget),
114
+ NameObject(F): NumberObject(4),
115
+ NameObject(MK): DictionaryObject(
116
+ {
117
+ NameObject(TP): NumberObject(1),
118
+ NameObject(IF): DictionaryObject(
119
+ {NameObject(S): NameObject(A)}
120
+ ),
121
+ NameObject(BC): ArrayObject(
122
+ FloatObject(value) for value in border_color
123
+ ),
124
+ }
125
+ ),
126
+ NameObject(BS): DictionaryObject(
127
+ {
128
+ NameObject(S): NameObject(S),
129
+ NameObject(W): NumberObject(1),
130
+ }
131
+ ),
132
+ NameObject(A): DictionaryObject(
133
+ {
134
+ NameObject(PdfType): NameObject(Action),
135
+ NameObject(S): NameObject(JavaScript),
136
+ NameObject(JS): TextStringObject(IMAGE_IMPORT_JAVASCRIPT),
137
+ }
138
+ ),
139
+ NameObject(DA): TextStringObject("/Micr 12 Tf 0 0 0 rg"),
140
+ NameObject(Rect): ImageWidget._build_rectangle(widget, width, height),
141
+ NameObject(AP): DictionaryObject(
142
+ {
143
+ NameObject(N): appearance,
144
+ NameObject(R): appearance,
145
+ NameObject(D): appearance,
146
+ }
147
+ ),
148
+ NameObject(T): TextStringObject(widget.name),
149
+ }
150
+ )
151
+ return out._add_object( # type: ignore # noqa: SLF001 # pylint: disable=W0212
152
+ annotation
153
+ )
154
+
155
+ @staticmethod
156
+ def bulk_watermarks(widgets: List[SignatureWidget], stream: bytes) -> List[bytes]:
157
+ """
158
+ Constructs image widgets in page-aligned carrier PDFs.
159
+
160
+ Each image field is built as a push-button annotation whose JavaScript
161
+ action invokes `buttonImportIcon()` in viewers that support Acrobat
162
+ JavaScript. A single border-only appearance stream is reused for the
163
+ normal, rollover, and pressed states. It has a transparent interior and
164
+ the same dark-gray, one-point solid border as the signature widget.
165
+ ``build_widget_watermarks`` packages the annotations by source page.
166
+
167
+ Args:
168
+ widgets (List[SignatureWidget]): Image widgets to construct.
169
+ stream (bytes): Source PDF used to determine page count and page size.
170
+
171
+ Returns:
172
+ List[bytes]: Page-aligned PDF streams containing the constructed
173
+ image annotations.
174
+ """
175
+ return ImageWidget.build_widget_watermarks(
176
+ widgets, stream, ImageWidget._build_annotation
177
+ )
178
+
179
+
180
+ @dataclass
181
+ class ImageField(SignatureField):
182
+ """
183
+ Represents an image field in a PDF document.
184
+
185
+ This dataclass extends `SignatureField` and selects `ImageWidget` as its
186
+ widget implementation. It inherits the optional width and height values;
187
+ when omitted, the widget resolves them to 160 and 90 points.
188
+
189
+ Attributes:
190
+ _widget_class (Type[ImageWidget]): The widget class associated with this field type.
191
+ """
192
+
193
+ _widget_class: Type[ImageWidget] = ImageWidget
@@ -0,0 +1,353 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module defines the `SignatureField` and `SignatureWidget` classes, which are
4
+ used to describe and construct signature form fields.
5
+
6
+ `SignatureField` stores the user-facing field definition. `SignatureWidget`
7
+ normalizes that definition's values, constructs a `/Sig` widget annotation,
8
+ and packages annotations in page-aligned PDFs so they can be copied into the
9
+ destination document.
10
+
11
+ Signature annotations and their appearance streams are constructed directly;
12
+ they do not depend on ReportLab's AcroForm API or an external template PDF.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections import defaultdict
18
+ from dataclasses import dataclass
19
+ from io import BytesIO
20
+ from typing import Any, Callable, List, Optional, Type
21
+
22
+ from pypdf import PdfReader, PdfWriter
23
+ from pypdf.generic import (
24
+ ArrayObject,
25
+ DictionaryObject,
26
+ FloatObject,
27
+ NameObject,
28
+ NumberObject,
29
+ StreamObject,
30
+ TextStringObject,
31
+ )
32
+
33
+ from ..constants import (
34
+ AP,
35
+ BC,
36
+ BS,
37
+ DA,
38
+ FT,
39
+ MK,
40
+ Annot,
41
+ Annots,
42
+ BBox,
43
+ F,
44
+ Form,
45
+ H,
46
+ N,
47
+ Q,
48
+ Rect,
49
+ Resources,
50
+ S,
51
+ Sig,
52
+ Subtype,
53
+ T,
54
+ W,
55
+ Widget,
56
+ XObject,
57
+ )
58
+ from ..constants import Type as PdfType
59
+ from .base import Field
60
+ from .base import Widget as BaseWidget
61
+
62
+
63
+ class SignatureWidget:
64
+ """
65
+ Represents a signature widget in a PDF form.
66
+
67
+ The widget stores placement, dimensions, and deferred hook values for a
68
+ signature field. Unlike widgets backed by ReportLab's AcroForm API, it
69
+ constructs its PDF annotation and normal appearance stream directly.
70
+
71
+ Attributes:
72
+ OPTIONAL_PARAMS (list): Width and height parameters with their defaults.
73
+ ALLOWED_HOOK_PARAMS (list): Parameters applied after the annotation has
74
+ been copied into the destination PDF.
75
+ """
76
+
77
+ OPTIONAL_PARAMS = [
78
+ ("width", 160),
79
+ ("height", 90),
80
+ ]
81
+ ALLOWED_HOOK_PARAMS = ["required", "tooltip"]
82
+
83
+ def __init__(
84
+ self,
85
+ name: str,
86
+ page_number: int,
87
+ x: float,
88
+ y: float,
89
+ **kwargs,
90
+ ) -> None:
91
+ """
92
+ Initializes a signature widget description.
93
+
94
+ The widget records placement information, resolves width and height with
95
+ defaults of 160 and 90 points, and captures `required` and `tooltip`
96
+ values for later application.
97
+
98
+ Args:
99
+ name (str): The name of the signature widget.
100
+ page_number (int): The 1-based destination page number.
101
+ x (float): The left edge of the widget in PDF page coordinates.
102
+ y (float): The bottom edge of the widget in PDF page coordinates.
103
+ **kwargs: Optional `width`, `height`, `required`, and `tooltip`
104
+ values.
105
+ """
106
+ super().__init__()
107
+ self.hook_params = []
108
+
109
+ self.page_number = page_number
110
+ self.name = name
111
+ self.x = x
112
+ self.y = y
113
+ self.optional_parameters = {
114
+ each[0]: kwargs.get(each[0], each[1]) for each in self.OPTIONAL_PARAMS
115
+ }
116
+ for each in self.ALLOWED_HOOK_PARAMS:
117
+ if each in kwargs:
118
+ self.hook_params.append((each, kwargs.get(each)))
119
+
120
+ @staticmethod
121
+ def build_widget_watermarks(
122
+ widgets: List[SignatureWidget],
123
+ stream: bytes,
124
+ annotation_builder: Callable[[PdfWriter, SignatureWidget], Any],
125
+ ) -> List[bytes]:
126
+ """
127
+ Builds page-aligned carrier PDFs from widget annotation objects.
128
+
129
+ Widgets are grouped by their 1-based page number. For every page that
130
+ contains widgets, this method creates a blank, single-page PDF sized
131
+ from the source page's media box. It asks ``annotation_builder`` to
132
+ create each annotation and its dependent objects in that PDF's writer,
133
+ then stores the returned annotation references in the page's `/Annots`
134
+ array. Pages without widgets are represented by an empty byte string.
135
+
136
+ Args:
137
+ widgets (List[SignatureWidget]): Widgets to package into carrier PDFs.
138
+ stream (bytes): Source PDF used to determine page count and page size.
139
+ annotation_builder (Callable): Function that receives the destination
140
+ writer and a widget, creates the annotation and its dependent
141
+ objects, and returns the annotation object or reference.
142
+
143
+ Returns:
144
+ List[bytes]: Page-aligned PDF streams. Each non-empty entry is
145
+ a single-page PDF containing the annotations for that source page.
146
+ """
147
+ page_to_widgets = defaultdict(list)
148
+ for widget in widgets:
149
+ page_to_widgets[widget.page_number].append(widget)
150
+
151
+ input_pdf = PdfReader(BytesIO(stream))
152
+ page_count = len(input_pdf.pages)
153
+ result = [b""] * page_count
154
+
155
+ for page_num in range(1, page_count + 1):
156
+ page_widgets = page_to_widgets.get(page_num, [])
157
+ if not page_widgets:
158
+ continue
159
+
160
+ page = input_pdf.pages[page_num - 1]
161
+ watermark, canvas = BaseWidget.create_watermark_canvas(page)
162
+ canvas.showPage()
163
+ canvas.save()
164
+ watermark.seek(0)
165
+
166
+ out = PdfWriter(watermark)
167
+ annotations = [annotation_builder(out, widget) for widget in page_widgets]
168
+ out.pages[0][NameObject(Annots)] = ArrayObject( # pylint: disable=E1137
169
+ annotations
170
+ )
171
+
172
+ with BytesIO() as result_stream:
173
+ out.write(result_stream)
174
+ result_stream.seek(0)
175
+ result[page_num - 1] = result_stream.read()
176
+
177
+ return result
178
+
179
+ @staticmethod
180
+ def _build_border_appearance(
181
+ width: float,
182
+ height: float,
183
+ border_color: tuple[float, float, float],
184
+ ) -> StreamObject:
185
+ """
186
+ Constructs the border-only appearance shared by signature and image widgets.
187
+
188
+ Args:
189
+ width (float): The width of the widget.
190
+ height (float): The height of the widget.
191
+ border_color (tuple): The RGB color of the widget border.
192
+
193
+ Returns:
194
+ StreamObject: An unregistered Form XObject containing the widget border.
195
+ """
196
+ appearance = StreamObject()
197
+ appearance.set_data(
198
+ (
199
+ f"{border_color[0]:g} "
200
+ f"{border_color[1]:g} "
201
+ f"{border_color[2]:g} RG\n"
202
+ "1 w\n"
203
+ f"0.5 0.5 {width - 1:g} {height - 1:g} re\n"
204
+ "s\n"
205
+ ).encode()
206
+ )
207
+ appearance.update(
208
+ {
209
+ NameObject(PdfType): NameObject(XObject),
210
+ NameObject(Subtype): NameObject(Form),
211
+ NameObject(BBox): ArrayObject(
212
+ [
213
+ FloatObject(0),
214
+ FloatObject(0),
215
+ FloatObject(width),
216
+ FloatObject(height),
217
+ ]
218
+ ),
219
+ NameObject(Resources): DictionaryObject(),
220
+ }
221
+ )
222
+ return appearance
223
+
224
+ @staticmethod
225
+ def _build_rectangle(
226
+ widget: SignatureWidget, width: float, height: float
227
+ ) -> ArrayObject:
228
+ """
229
+ Constructs the annotation rectangle shared by signature and image widgets.
230
+
231
+ Args:
232
+ widget (SignatureWidget): The widget whose placement defines the rectangle.
233
+ width (float): The width of the widget.
234
+ height (float): The height of the widget.
235
+
236
+ Returns:
237
+ ArrayObject: The annotation's lower-left and upper-right coordinates.
238
+ """
239
+ return ArrayObject(
240
+ [
241
+ FloatObject(widget.x),
242
+ FloatObject(widget.y),
243
+ FloatObject(widget.x + width),
244
+ FloatObject(widget.y + height),
245
+ ]
246
+ )
247
+
248
+ @staticmethod
249
+ def _build_annotation(out: PdfWriter, widget: SignatureWidget) -> Any:
250
+ """
251
+ Constructs a signature widget annotation owned by a PDF writer.
252
+
253
+ This method creates a border-only Form XObject for the normal appearance,
254
+ builds a `/Sig` widget from the normalized name, position, and dimensions,
255
+ and registers both objects with the same writer. It implements the
256
+ annotation-builder callback used by `build_widget_watermarks`.
257
+
258
+ Args:
259
+ out (PdfWriter): The writer that will own the appearance stream and
260
+ widget annotation.
261
+ widget (SignatureWidget): The normalized signature widget definition
262
+ to convert into a PDF annotation.
263
+
264
+ Returns:
265
+ Any: The writer-owned indirect reference to the widget annotation.
266
+ """
267
+ width = float(widget.optional_parameters["width"])
268
+ height = float(widget.optional_parameters["height"])
269
+ border_color = (0.1, 0.1, 0.1)
270
+
271
+ appearance = SignatureWidget._build_border_appearance(
272
+ width, height, border_color
273
+ )
274
+ appearance_ref = out._add_object( # type: ignore # noqa: SLF001 # pylint: disable=W0212
275
+ appearance.flate_encode()
276
+ )
277
+
278
+ annotation = DictionaryObject(
279
+ {
280
+ NameObject(PdfType): NameObject(Annot),
281
+ NameObject(Subtype): NameObject(Widget),
282
+ NameObject(Rect): SignatureWidget._build_rectangle(
283
+ widget, width, height
284
+ ),
285
+ NameObject(MK): DictionaryObject(
286
+ {
287
+ NameObject(BC): ArrayObject(
288
+ FloatObject(value) for value in border_color
289
+ )
290
+ }
291
+ ),
292
+ NameObject(BS): DictionaryObject(
293
+ {
294
+ NameObject(S): NameObject(S),
295
+ NameObject(W): NumberObject(1),
296
+ }
297
+ ),
298
+ NameObject(AP): DictionaryObject({NameObject(N): appearance_ref}),
299
+ NameObject(DA): TextStringObject("/Helv 0 Tf 0 g"),
300
+ NameObject(F): NumberObject(4),
301
+ NameObject(FT): NameObject(Sig),
302
+ NameObject(H): NameObject(N),
303
+ NameObject(T): TextStringObject(widget.name),
304
+ NameObject(Q): NumberObject(0),
305
+ }
306
+ )
307
+ return out._add_object( # type: ignore # noqa: SLF001 # pylint: disable=W0212
308
+ annotation
309
+ )
310
+
311
+ @staticmethod
312
+ def bulk_watermarks(widgets: List[SignatureWidget], stream: bytes) -> List[bytes]:
313
+ """
314
+ Constructs signature widgets in page-aligned carrier PDFs.
315
+
316
+ Each widget is represented by a `/Sig` annotation with a transparent
317
+ interior and a dark-gray, one-point solid border. Its normal appearance
318
+ stream contains only that border. The annotation and appearance are
319
+ created in the carrier PDF's writer, so they do not retain references
320
+ to another PDF. ``build_widget_watermarks`` then packages the
321
+ annotations by source page.
322
+
323
+ Args:
324
+ widgets (List[SignatureWidget]): Signature widgets to construct.
325
+ stream (bytes): Source PDF used to determine page count and page size.
326
+
327
+ Returns:
328
+ List[bytes]: Page-aligned PDF streams containing the constructed
329
+ signature annotations.
330
+ """
331
+ return SignatureWidget.build_widget_watermarks(
332
+ widgets, stream, SignatureWidget._build_annotation
333
+ )
334
+
335
+
336
+ @dataclass
337
+ class SignatureField(Field):
338
+ """
339
+ Represents a signature field in a PDF document.
340
+
341
+ This dataclass extends the `Field` base class and defines the specific
342
+ dimensions that can be configured for a signature input field.
343
+
344
+ Attributes:
345
+ _widget_class (Type[SignatureWidget]): The widget class associated with this field type.
346
+ width (Optional[float]): The width of the signature field.
347
+ height (Optional[float]): The height of the signature field.
348
+ """
349
+
350
+ _widget_class: Type[SignatureWidget] = SignatureWidget
351
+
352
+ width: Optional[float] = None
353
+ height: Optional[float] = None