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.
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PKG-INFO +1 -1
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/__init__.py +1 -1
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/constants.py +13 -1
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/egress.py +2 -2
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/patterns.py +2 -2
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/base.py +24 -11
- pypdfform-5.5.3/PyPDFForm/lib/widgets/image.py +193 -0
- pypdfform-5.5.3/PyPDFForm/lib/widgets/signature.py +353 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/wrapper.py +6 -12
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/PKG-INFO +1 -1
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/SOURCES.txt +0 -1
- pypdfform-5.5.2/PyPDFForm/lib/assets/bedrock.py +0 -4
- pypdfform-5.5.2/PyPDFForm/lib/widgets/image.py +0 -50
- pypdfform-5.5.2/PyPDFForm/lib/widgets/signature.py +0 -201
- {pypdfform-5.5.2 → pypdfform-5.5.3}/LICENSE +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/common.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/create.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/inspect.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/remove.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/root.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/cli/update.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/font.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/template.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/watermark.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/requires.txt +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/README.md +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/pyproject.toml +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/setup.cfg +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_create_widget.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_draw_elements.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_dropdown.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_font_widths.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_functional.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_js.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_need_appearances.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_paragraph.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_signature.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_use_full_widget_name.py +0 -0
- {pypdfform-5.5.2 → pypdfform-5.5.3}/tests/test_widget_attr_trigger.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|