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.
- PyPDFForm/__init__.py +5 -3
- PyPDFForm/adapter.py +33 -1
- PyPDFForm/ap.py +99 -0
- PyPDFForm/assets/__init__.py +0 -0
- PyPDFForm/assets/blank.py +100 -0
- PyPDFForm/constants.py +20 -2
- PyPDFForm/coordinate.py +7 -11
- PyPDFForm/deprecation.py +30 -0
- PyPDFForm/filler.py +17 -36
- PyPDFForm/font.py +16 -16
- PyPDFForm/hooks.py +153 -30
- PyPDFForm/image.py +0 -3
- PyPDFForm/middleware/__init__.py +35 -0
- PyPDFForm/middleware/base.py +24 -5
- PyPDFForm/middleware/checkbox.py +18 -1
- PyPDFForm/middleware/signature.py +0 -1
- PyPDFForm/patterns.py +44 -13
- PyPDFForm/raw/__init__.py +37 -0
- PyPDFForm/raw/circle.py +65 -0
- PyPDFForm/raw/ellipse.py +69 -0
- PyPDFForm/raw/image.py +79 -0
- PyPDFForm/raw/line.py +65 -0
- PyPDFForm/raw/rect.py +70 -0
- PyPDFForm/raw/text.py +73 -0
- PyPDFForm/template.py +114 -12
- PyPDFForm/types.py +49 -0
- PyPDFForm/utils.py +31 -41
- PyPDFForm/watermark.py +153 -44
- PyPDFForm/widgets/__init__.py +1 -0
- PyPDFForm/widgets/base.py +79 -59
- PyPDFForm/widgets/checkbox.py +30 -30
- PyPDFForm/widgets/dropdown.py +42 -40
- PyPDFForm/widgets/image.py +17 -16
- PyPDFForm/widgets/radio.py +27 -28
- PyPDFForm/widgets/signature.py +96 -60
- PyPDFForm/widgets/text.py +40 -40
- PyPDFForm/wrapper.py +256 -240
- {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/METADATA +33 -26
- pypdfform-4.2.0.dist-info/RECORD +47 -0
- {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/licenses/LICENSE +1 -1
- pypdfform-3.5.3.dist-info/RECORD +0 -35
- /PyPDFForm/{widgets → assets}/bedrock.py +0 -0
- {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/WHEEL +0 -0
- {pypdfform-3.5.3.dist-info → pypdfform-4.2.0.dist-info}/top_level.txt +0 -0
PyPDFForm/wrapper.py
CHANGED
|
@@ -15,51 +15,41 @@ methods for interacting with its form fields and content. It leverages
|
|
|
15
15
|
lower-level modules within the `PyPDFForm` library to handle the
|
|
16
16
|
underlying PDF manipulation.
|
|
17
17
|
"""
|
|
18
|
-
# TODO: The `__add__` method (merging PDFs) involves multiple `self.read()` and `other.read()` calls, leading to redundant PDF parsing. Consider optimizing by passing `PdfReader` objects directly or by performing a single read and then merging.
|
|
19
|
-
# TODO: In `_init_helper`, `build_widgets` and `get_all_available_fonts` both call `self.read()`, causing the PDF to be parsed multiple times. Optimize by parsing the PDF once and passing the `PdfReader` object to these functions.
|
|
20
|
-
# TODO: The `pages` property's implementation involves `get_page_streams(remove_all_widgets(self.read()))` and `copy_watermark_widgets(each, self.read(), None, i)`. This leads to excessive PDF parsing, widget removal, and copying for each page. Refactor to minimize PDF I/O operations, possibly by working with `pypdf` page objects directly.
|
|
21
|
-
# TODO: The `read` method triggers `trigger_widget_hooks` and `enable_adobe_mode`, both of which can involve PDF parsing and writing. Since `read` is called frequently, this can be a performance bottleneck. Consider a more granular dirty-flag system to only apply changes when necessary, or accumulate changes and apply them in a single PDF write operation.
|
|
22
|
-
# TODO: The `write` method calls `self.read()`, which in turn triggers all pending operations. This can lead to redundant processing if `read()` has already been called or if multiple `write()` calls are made.
|
|
23
|
-
# TODO: In `change_version`, replacing a byte string in the entire PDF stream can be inefficient for very large PDFs. Consider if `pypdf` offers a more direct way to update the PDF version without full stream manipulation.
|
|
24
|
-
# TODO: In `generate_coordinate_grid`, `self.read()` is called multiple times, and then `remove_all_widgets`, `generate_coordinate_grid`, and `copy_watermark_widgets` are called, all of which involve PDF parsing and manipulation. Optimize by minimizing PDF I/O and object re-creation.
|
|
25
|
-
# TODO: In `fill`, `self.read()` is called, and then `fill` (from `filler.py`), `remove_all_widgets`, and `copy_watermark_widgets` are called. This is a major operation and likely a performance hotspot due to repeated PDF processing. Streamline the PDF modification workflow to reduce redundant parsing and writing.
|
|
26
|
-
# TODO: In `create_widget`, `obj.watermarks(self.read())` and `copy_watermark_widgets(self.read(), watermarks, [name], None)` involve reading the PDF multiple times. Optimize by passing the PDF stream or `PdfReader` object more efficiently.
|
|
27
|
-
# TODO: The `commit_widget_key_updates` method calls `update_widget_keys`, which involves re-parsing and writing the PDF. For bulk updates, consider a mechanism to apply all key changes in a single PDF modification operation.
|
|
28
|
-
# TODO: General: Many methods repeatedly call `self.read()`, which re-parses the PDF. Consider maintaining a persistent `pypdf.PdfReader` and `pypdf.PdfWriter` object internally and only writing to a byte stream when explicitly requested (e.g., by the `read()` or `write()` methods) to avoid redundant I/O and parsing overhead.
|
|
29
18
|
|
|
30
19
|
from __future__ import annotations
|
|
31
20
|
|
|
21
|
+
from collections import defaultdict
|
|
32
22
|
from dataclasses import asdict
|
|
33
23
|
from functools import cached_property
|
|
34
|
-
from
|
|
35
|
-
from
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
from .
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
from os import PathLike
|
|
25
|
+
from typing import (TYPE_CHECKING, BinaryIO, Dict, Sequence, TextIO, Tuple,
|
|
26
|
+
Union)
|
|
27
|
+
|
|
28
|
+
from .adapter import (fp_or_f_obj_or_f_content_to_content,
|
|
29
|
+
fp_or_f_obj_or_stream_to_stream)
|
|
30
|
+
from .ap import appearance_streams_handler
|
|
31
|
+
from .constants import VERSION_IDENTIFIER_PREFIX, VERSION_IDENTIFIERS
|
|
41
32
|
from .coordinate import generate_coordinate_grid
|
|
42
33
|
from .filler import fill
|
|
43
34
|
from .font import (get_all_available_fonts, register_font,
|
|
44
35
|
register_font_acroform)
|
|
45
36
|
from .hooks import trigger_widget_hooks
|
|
46
|
-
from .image import rotate_image
|
|
47
37
|
from .middleware.dropdown import Dropdown
|
|
48
38
|
from .middleware.signature import Signature
|
|
49
39
|
from .middleware.text import Text
|
|
50
|
-
from .
|
|
51
|
-
from .
|
|
52
|
-
|
|
40
|
+
from .raw import RawText, RawTypes
|
|
41
|
+
from .template import (build_widgets, get_on_open_javascript, get_pdf_title,
|
|
42
|
+
set_on_open_javascript, set_pdf_title,
|
|
43
|
+
update_widget_keys)
|
|
44
|
+
from .types import PdfWrapperList
|
|
45
|
+
from .utils import (generate_unique_suffix, get_page_streams, merge_pdfs,
|
|
46
|
+
remove_all_widgets)
|
|
53
47
|
from .watermark import (copy_watermark_widgets, create_watermarks_and_draw,
|
|
54
48
|
merge_watermarks_with_pdf)
|
|
55
|
-
from .widgets
|
|
56
|
-
from .widgets.dropdown import DropdownWidget
|
|
57
|
-
from .widgets.image import ImageWidget
|
|
58
|
-
from .widgets.radio import RadioWidget
|
|
59
|
-
from .widgets.signature import SignatureWidget
|
|
60
|
-
from .widgets.text import TextWidget
|
|
49
|
+
from .widgets import CheckBoxField, ImageField, RadioGroup, SignatureField
|
|
61
50
|
|
|
62
51
|
if TYPE_CHECKING:
|
|
52
|
+
from .assets.blank import BlankPage
|
|
63
53
|
from .widgets import FieldTypes
|
|
64
54
|
|
|
65
55
|
|
|
@@ -78,18 +68,22 @@ class PdfWrapper:
|
|
|
78
68
|
These parameters can be set during initialization using keyword arguments.
|
|
79
69
|
Current parameters include:
|
|
80
70
|
- `use_full_widget_name` (bool): Whether to use the full widget name when filling the form.
|
|
81
|
-
- `
|
|
71
|
+
- `need_appearances` (bool): Whether to set the `NeedAppearances` flag in the PDF's AcroForm dictionary.
|
|
72
|
+
- `generate_appearance_streams` (bool): Whether to explicitly generate appearance streams for all form fields using pikepdf.
|
|
73
|
+
- `title` (str): The title of the PDF document.
|
|
82
74
|
|
|
83
75
|
"""
|
|
84
76
|
|
|
85
77
|
USER_PARAMS = [
|
|
86
78
|
("use_full_widget_name", False),
|
|
87
|
-
("
|
|
79
|
+
("need_appearances", False),
|
|
80
|
+
("generate_appearance_streams", False),
|
|
81
|
+
("title", None),
|
|
88
82
|
]
|
|
89
83
|
|
|
90
84
|
def __init__(
|
|
91
85
|
self,
|
|
92
|
-
template: Union[bytes, str, BinaryIO] = b"",
|
|
86
|
+
template: Union[bytes, str, BinaryIO, BlankPage] = b"",
|
|
93
87
|
**kwargs,
|
|
94
88
|
) -> None:
|
|
95
89
|
"""
|
|
@@ -98,14 +92,15 @@ class PdfWrapper:
|
|
|
98
92
|
Initializes a new `PdfWrapper` object with the given template PDF and optional keyword arguments.
|
|
99
93
|
|
|
100
94
|
Args:
|
|
101
|
-
template (Union[bytes, str, BinaryIO]): The template PDF, provided as either:
|
|
95
|
+
template (Union[bytes, str, BinaryIO, BlankPage]): The template PDF, provided as either:
|
|
102
96
|
- bytes: The raw PDF data as a byte string.
|
|
103
97
|
- str: The file path to the PDF.
|
|
104
98
|
- BinaryIO: An open file-like object containing the PDF data.
|
|
99
|
+
- BlankPage: A blank page object.
|
|
105
100
|
Defaults to an empty byte string (b""), which creates a blank PDF.
|
|
106
101
|
**kwargs: Additional keyword arguments to configure the `PdfWrapper`.
|
|
107
102
|
These arguments are used to set the user-configurable parameters defined in `USER_PARAMS`.
|
|
108
|
-
For example: `use_full_widget_name=True` or `
|
|
103
|
+
For example: `use_full_widget_name=True` or `need_appearances=False`.
|
|
109
104
|
"""
|
|
110
105
|
|
|
111
106
|
super().__init__()
|
|
@@ -120,26 +115,37 @@ class PdfWrapper:
|
|
|
120
115
|
for attr, default in self.USER_PARAMS:
|
|
121
116
|
setattr(self, attr, kwargs.get(attr, default))
|
|
122
117
|
|
|
118
|
+
if getattr(self, "generate_appearance_streams") is True:
|
|
119
|
+
self.need_appearances = True
|
|
120
|
+
|
|
123
121
|
self._init_helper()
|
|
124
122
|
|
|
125
|
-
def __add__(self, other: PdfWrapper) -> PdfWrapper:
|
|
123
|
+
def __add__(self, other: Union[PdfWrapper, Sequence[PdfWrapper]]) -> PdfWrapper:
|
|
126
124
|
"""
|
|
127
|
-
Merges
|
|
125
|
+
Merges PDF wrappers together, creating a new `PdfWrapper` containing the combined content.
|
|
128
126
|
|
|
129
|
-
This method allows you to combine
|
|
130
|
-
naming conflicts between form fields by adding a unique suffix to the field names in the
|
|
127
|
+
This method allows you to combine PDF forms into a single form. It handles potential
|
|
128
|
+
naming conflicts between form fields by adding a unique suffix to the field names in the
|
|
129
|
+
form being merged.
|
|
131
130
|
|
|
132
131
|
Args:
|
|
133
|
-
other (PdfWrapper): The other `PdfWrapper` object
|
|
132
|
+
other (Union[PdfWrapper, Sequence[PdfWrapper]]): The other `PdfWrapper` object or
|
|
133
|
+
a sequence of `PdfWrapper` objects to merge with.
|
|
134
134
|
|
|
135
135
|
Returns:
|
|
136
136
|
PdfWrapper: A new `PdfWrapper` object containing the merged PDFs.
|
|
137
137
|
"""
|
|
138
138
|
|
|
139
|
-
if
|
|
139
|
+
if isinstance(other, Sequence):
|
|
140
|
+
result = self
|
|
141
|
+
for each in other:
|
|
142
|
+
result += each
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
if not self or not self._read():
|
|
140
146
|
return other
|
|
141
147
|
|
|
142
|
-
if not other.
|
|
148
|
+
if not other or not other._read():
|
|
143
149
|
return self
|
|
144
150
|
|
|
145
151
|
unique_suffix = generate_unique_suffix()
|
|
@@ -151,7 +157,7 @@ class PdfWrapper:
|
|
|
151
157
|
|
|
152
158
|
# user params are based on the first object
|
|
153
159
|
result = self.__class__(
|
|
154
|
-
|
|
160
|
+
merge_pdfs([self._read(), other._read()]),
|
|
155
161
|
**{each[0]: getattr(self, each[0], each[1]) for each in self.USER_PARAMS},
|
|
156
162
|
)
|
|
157
163
|
|
|
@@ -172,10 +178,10 @@ class PdfWrapper:
|
|
|
172
178
|
|
|
173
179
|
new_widgets = (
|
|
174
180
|
build_widgets(
|
|
175
|
-
self.
|
|
181
|
+
self._read(),
|
|
176
182
|
getattr(self, "use_full_widget_name"),
|
|
177
183
|
)
|
|
178
|
-
if self.
|
|
184
|
+
if self._read()
|
|
179
185
|
else {}
|
|
180
186
|
)
|
|
181
187
|
# ensure old widgets don't get overwritten
|
|
@@ -195,8 +201,8 @@ class PdfWrapper:
|
|
|
195
201
|
|
|
196
202
|
self.widgets = new_widgets
|
|
197
203
|
|
|
198
|
-
if self.
|
|
199
|
-
self._available_fonts.update(**get_all_available_fonts(self.
|
|
204
|
+
if self._read():
|
|
205
|
+
self._available_fonts.update(**get_all_available_fonts(self._read()))
|
|
200
206
|
|
|
201
207
|
def _reregister_font(self) -> PdfWrapper:
|
|
202
208
|
"""
|
|
@@ -217,6 +223,28 @@ class PdfWrapper:
|
|
|
217
223
|
|
|
218
224
|
return self
|
|
219
225
|
|
|
226
|
+
@property
|
|
227
|
+
def title(self) -> Union[str, None]:
|
|
228
|
+
"""
|
|
229
|
+
Returns the title of the PDF document.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Union[str, None]: The title of the PDF, or None if it's not set.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
return get_pdf_title(self._read())
|
|
236
|
+
|
|
237
|
+
@title.setter
|
|
238
|
+
def title(self, value: str) -> None:
|
|
239
|
+
"""
|
|
240
|
+
Sets the title of the PDF document.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
value (str): The new title for the PDF document.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
self._stream = set_pdf_title(self._read(), value)
|
|
247
|
+
|
|
220
248
|
@property
|
|
221
249
|
def schema(self) -> dict:
|
|
222
250
|
"""
|
|
@@ -274,7 +302,7 @@ class PdfWrapper:
|
|
|
274
302
|
"""
|
|
275
303
|
|
|
276
304
|
for each in VERSION_IDENTIFIERS:
|
|
277
|
-
if self.
|
|
305
|
+
if self._read().startswith(each):
|
|
278
306
|
return each.replace(VERSION_IDENTIFIER_PREFIX, b"").decode()
|
|
279
307
|
|
|
280
308
|
return None
|
|
@@ -293,20 +321,20 @@ class PdfWrapper:
|
|
|
293
321
|
@cached_property
|
|
294
322
|
def pages(self) -> Sequence[PdfWrapper]:
|
|
295
323
|
"""
|
|
296
|
-
Returns a
|
|
324
|
+
Returns a list of `PdfWrapper` objects, each representing a single page in the PDF document.
|
|
297
325
|
|
|
298
326
|
This allows you to work with individual pages of the PDF, for example, to extract text or images from a specific page.
|
|
299
327
|
|
|
300
328
|
Returns:
|
|
301
|
-
Sequence[PdfWrapper]: A
|
|
329
|
+
Sequence[PdfWrapper]: A list of `PdfWrapper` objects, one for each page in the PDF.
|
|
302
330
|
"""
|
|
303
331
|
|
|
304
332
|
result = [
|
|
305
333
|
self.__class__(
|
|
306
|
-
copy_watermark_widgets(each, self.
|
|
334
|
+
copy_watermark_widgets(each, self._read(), None, i),
|
|
307
335
|
**{param: getattr(self, param) for param, _ in self.USER_PARAMS},
|
|
308
336
|
)
|
|
309
|
-
for i, each in enumerate(get_page_streams(remove_all_widgets(self.
|
|
337
|
+
for i, each in enumerate(get_page_streams(remove_all_widgets(self._read())))
|
|
310
338
|
]
|
|
311
339
|
|
|
312
340
|
# because copy_watermark_widgets and remove_all_widgets
|
|
@@ -315,17 +343,62 @@ class PdfWrapper:
|
|
|
315
343
|
for page in result:
|
|
316
344
|
page.register_font(event[0], event[1])
|
|
317
345
|
|
|
318
|
-
return result
|
|
346
|
+
return PdfWrapperList(result)
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def on_open_javascript(self) -> Union[str, None]:
|
|
350
|
+
"""
|
|
351
|
+
Returns the JavaScript that runs when the PDF is opened.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Union[str, None]: The JavaScript that runs when the PDF is opened, or None if it's not set.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
return get_on_open_javascript(self._read())
|
|
358
|
+
|
|
359
|
+
@on_open_javascript.setter
|
|
360
|
+
def on_open_javascript(self, value: Union[str, TextIO]) -> None:
|
|
361
|
+
"""
|
|
362
|
+
Sets the JavaScript that runs when the PDF is opened.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
value (Union[str, TextIO]): The JavaScript to run when the PDF is opened.
|
|
366
|
+
Can be a string or a text file-like object.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
self._stream = set_on_open_javascript(
|
|
370
|
+
self._read(), fp_or_f_obj_or_f_content_to_content(value)
|
|
371
|
+
)
|
|
319
372
|
|
|
320
373
|
def read(self) -> bytes:
|
|
321
374
|
"""
|
|
322
|
-
Reads the PDF
|
|
375
|
+
Reads the PDF document and returns its content as bytes.
|
|
376
|
+
|
|
377
|
+
This method retrieves the PDF stream and optionally generates appearance
|
|
378
|
+
streams for form fields if `need_appearances` is enabled.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
bytes: The PDF document content as a byte string.
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
result = self._read()
|
|
385
|
+
if getattr(self, "need_appearances") and result:
|
|
386
|
+
result = appearance_streams_handler(
|
|
387
|
+
result, getattr(self, "generate_appearance_streams")
|
|
388
|
+
) # cached
|
|
389
|
+
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
def _read(self) -> bytes:
|
|
393
|
+
"""
|
|
394
|
+
Reads the PDF stream, triggering widget hooks and updating fonts if necessary.
|
|
323
395
|
|
|
324
|
-
This method
|
|
325
|
-
|
|
396
|
+
This internal method ensures that all widget hooks are executed and that
|
|
397
|
+
fonts are correctly mapped to their internal PDF names before returning
|
|
398
|
+
the raw PDF stream.
|
|
326
399
|
|
|
327
400
|
Returns:
|
|
328
|
-
bytes: The PDF
|
|
401
|
+
bytes: The raw PDF stream.
|
|
329
402
|
"""
|
|
330
403
|
|
|
331
404
|
if any(widget.hooks_to_trigger for widget in self.widgets.values()):
|
|
@@ -345,24 +418,25 @@ class PdfWrapper:
|
|
|
345
418
|
getattr(self, "use_full_widget_name"),
|
|
346
419
|
)
|
|
347
420
|
|
|
348
|
-
if getattr(self, "adobe_mode") and self._stream:
|
|
349
|
-
self._stream = enable_adobe_mode(self._stream) # cached
|
|
350
|
-
|
|
351
421
|
return self._stream
|
|
352
422
|
|
|
353
|
-
def write(self,
|
|
423
|
+
def write(self, dest: Union[str, BinaryIO]) -> PdfWrapper:
|
|
354
424
|
"""
|
|
355
|
-
Writes the PDF
|
|
425
|
+
Writes the PDF to a file.
|
|
356
426
|
|
|
357
427
|
Args:
|
|
358
|
-
|
|
428
|
+
dest (Union[str, BinaryIO]): The destination to write the PDF to.
|
|
429
|
+
Can be a file path (str) or a file-like object (BinaryIO).
|
|
359
430
|
|
|
360
431
|
Returns:
|
|
361
432
|
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
362
433
|
"""
|
|
363
434
|
|
|
364
|
-
|
|
365
|
-
|
|
435
|
+
if isinstance(dest, (str, bytes, PathLike)):
|
|
436
|
+
with open(dest, "wb+") as f:
|
|
437
|
+
f.write(self.read())
|
|
438
|
+
else:
|
|
439
|
+
dest.write(self.read())
|
|
366
440
|
|
|
367
441
|
return self
|
|
368
442
|
|
|
@@ -377,7 +451,7 @@ class PdfWrapper:
|
|
|
377
451
|
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
378
452
|
"""
|
|
379
453
|
|
|
380
|
-
self._stream = self.
|
|
454
|
+
self._stream = self._read().replace(
|
|
381
455
|
VERSION_IDENTIFIER_PREFIX + bytes(self.version, "utf-8"),
|
|
382
456
|
VERSION_IDENTIFIER_PREFIX + bytes(version, "utf-8"),
|
|
383
457
|
1,
|
|
@@ -399,10 +473,10 @@ class PdfWrapper:
|
|
|
399
473
|
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
400
474
|
"""
|
|
401
475
|
|
|
402
|
-
stream_with_widgets = self.
|
|
476
|
+
stream_with_widgets = self._read()
|
|
403
477
|
self._stream = copy_watermark_widgets(
|
|
404
478
|
generate_coordinate_grid(
|
|
405
|
-
remove_all_widgets(self.
|
|
479
|
+
remove_all_widgets(self._read()),
|
|
406
480
|
color,
|
|
407
481
|
margin,
|
|
408
482
|
),
|
|
@@ -417,15 +491,16 @@ class PdfWrapper:
|
|
|
417
491
|
|
|
418
492
|
def fill(
|
|
419
493
|
self,
|
|
420
|
-
data: Dict[str, Union[str, bool, int]],
|
|
494
|
+
data: Dict[str, Union[str, bool, int, BinaryIO, bytes]],
|
|
421
495
|
**kwargs,
|
|
422
496
|
) -> PdfWrapper:
|
|
423
497
|
"""
|
|
424
498
|
Fills the PDF form with data from a dictionary.
|
|
425
499
|
|
|
426
500
|
Args:
|
|
427
|
-
data (Dict[str, Union[str, bool, int]]): A dictionary where keys
|
|
428
|
-
and values are the data to fill the fields with.
|
|
501
|
+
data (Dict[str, Union[str, bool, int, BinaryIO, bytes]]): A dictionary where keys
|
|
502
|
+
are form field names and values are the data to fill the fields with.
|
|
503
|
+
Values can be strings, booleans, integers, file-like objects, or bytes.
|
|
429
504
|
**kwargs: Additional keyword arguments:
|
|
430
505
|
- `flatten` (bool): Whether to flatten the form after filling, making the fields read-only (default: False).
|
|
431
506
|
|
|
@@ -438,8 +513,9 @@ class PdfWrapper:
|
|
|
438
513
|
self.widgets[key].value = value
|
|
439
514
|
|
|
440
515
|
filled_stream, image_drawn_stream = fill(
|
|
441
|
-
self.
|
|
516
|
+
self._read(),
|
|
442
517
|
self.widgets,
|
|
518
|
+
need_appearances=getattr(self, "need_appearances"),
|
|
443
519
|
use_full_widget_name=getattr(self, "use_full_widget_name"),
|
|
444
520
|
flatten=kwargs.get("flatten", False),
|
|
445
521
|
)
|
|
@@ -462,112 +538,125 @@ class PdfWrapper:
|
|
|
462
538
|
|
|
463
539
|
return self
|
|
464
540
|
|
|
465
|
-
def
|
|
466
|
-
self,
|
|
467
|
-
field: FieldTypes,
|
|
468
|
-
) -> PdfWrapper:
|
|
541
|
+
def bulk_create_fields(self, fields: Sequence[FieldTypes]) -> PdfWrapper:
|
|
469
542
|
"""
|
|
470
|
-
Creates
|
|
543
|
+
Creates multiple new form fields (widgets) on the PDF in a single operation.
|
|
471
544
|
|
|
472
|
-
This method
|
|
473
|
-
|
|
545
|
+
This method takes a list of field definition objects (`FieldTypes`),
|
|
546
|
+
groups them by type (if necessary for specific widget handling, like CheckBoxField),
|
|
547
|
+
and then delegates the creation to the internal `_bulk_create_fields` method.
|
|
548
|
+
This is the preferred method for creating multiple fields as it minimizes
|
|
549
|
+
PDF manipulation overhead.
|
|
474
550
|
|
|
475
551
|
Args:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
page number, coordinates, and type of the field.
|
|
552
|
+
fields (Sequence[FieldTypes]): A list of field definition objects
|
|
553
|
+
(e.g., `TextField`, `CheckBoxField`, etc.) to be created.
|
|
479
554
|
|
|
480
555
|
Returns:
|
|
481
556
|
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
482
557
|
"""
|
|
483
558
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
559
|
+
needs_separate_creation = [
|
|
560
|
+
CheckBoxField,
|
|
561
|
+
RadioGroup,
|
|
562
|
+
SignatureField,
|
|
563
|
+
ImageField,
|
|
564
|
+
]
|
|
565
|
+
needs_separate_creation_dict = defaultdict(list)
|
|
566
|
+
general_creation = []
|
|
567
|
+
|
|
568
|
+
for each in fields:
|
|
569
|
+
if type(each) in needs_separate_creation:
|
|
570
|
+
needs_separate_creation_dict[type(each)].append(each)
|
|
571
|
+
else:
|
|
572
|
+
general_creation.append(each)
|
|
573
|
+
|
|
574
|
+
needs_separate_creation_dict[SignatureField] = needs_separate_creation_dict.pop(
|
|
575
|
+
SignatureField, []
|
|
576
|
+
) + needs_separate_creation_dict.pop(ImageField, [])
|
|
577
|
+
needs_separate_creation_dict[CheckBoxField] = needs_separate_creation_dict.pop(
|
|
578
|
+
CheckBoxField, []
|
|
579
|
+
) + needs_separate_creation_dict.pop(RadioGroup, [])
|
|
580
|
+
|
|
581
|
+
for each in list(needs_separate_creation_dict.values()) + [general_creation]:
|
|
582
|
+
if each:
|
|
583
|
+
self._bulk_create_fields(each)
|
|
500
584
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
name: str,
|
|
505
|
-
page_number: int,
|
|
506
|
-
x: Union[float, List[float]],
|
|
507
|
-
y: Union[float, List[float]],
|
|
508
|
-
**kwargs,
|
|
509
|
-
) -> PdfWrapper:
|
|
585
|
+
return self
|
|
586
|
+
|
|
587
|
+
def _bulk_create_fields(self, fields: Sequence[FieldTypes]) -> PdfWrapper:
|
|
510
588
|
"""
|
|
511
|
-
|
|
589
|
+
Internal method to create multiple new form fields (widgets) on the PDF in a single operation.
|
|
590
|
+
|
|
591
|
+
This method takes a list of field definition objects (`FieldTypes`),
|
|
592
|
+
converts them into `Widget` objects, and efficiently draws them onto the
|
|
593
|
+
PDF using bulk watermarking. It is designed to be called by the public
|
|
594
|
+
`bulk_create_fields` method after fields have been grouped for creation.
|
|
512
595
|
|
|
513
596
|
Args:
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
- "checkbox": A checkbox field.
|
|
517
|
-
- "dropdown": A dropdown field.
|
|
518
|
-
- "radio": A radio button field.
|
|
519
|
-
- "signature": A signature field.
|
|
520
|
-
- "image": An image field.
|
|
521
|
-
name (str): The name of the widget. This name will be used to identify the widget when filling the form.
|
|
522
|
-
page_number (int): The page number to create the widget on (1-based).
|
|
523
|
-
x (Union[float, List[float]]): The x coordinate(s) of the widget.
|
|
524
|
-
If a list is provided, it specifies the x coordinates of multiple instances of the widget.
|
|
525
|
-
y (Union[float, List[float]]): The y coordinate(s) of the widget.
|
|
526
|
-
If a list is provided, it specifies the y coordinates of multiple instances of the widget.
|
|
527
|
-
**kwargs: Additional keyword arguments specific to the widget type.
|
|
597
|
+
fields (Sequence[FieldTypes]): A list of field definition objects
|
|
598
|
+
(e.g., `TextField`, `CheckBoxField`, etc.) to be created.
|
|
528
599
|
|
|
529
600
|
Returns:
|
|
530
601
|
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
531
602
|
"""
|
|
532
603
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
604
|
+
widgets = []
|
|
605
|
+
widget_class = None
|
|
606
|
+
for field in fields:
|
|
607
|
+
field_dict = asdict(field)
|
|
608
|
+
widget_class = getattr(field, "_widget_class")
|
|
609
|
+
name = field_dict.pop("name")
|
|
610
|
+
page_number = field_dict.pop("page_number")
|
|
611
|
+
x = field_dict.pop("x")
|
|
612
|
+
y = field_dict.pop("y")
|
|
613
|
+
widgets.append(
|
|
614
|
+
widget_class(
|
|
615
|
+
name=name,
|
|
616
|
+
page_number=page_number,
|
|
617
|
+
x=x,
|
|
618
|
+
y=y,
|
|
619
|
+
**{k: v for k, v in field_dict.items() if v is not None},
|
|
620
|
+
)
|
|
541
621
|
)
|
|
542
622
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if widget_type == "radio":
|
|
551
|
-
_class = RadioWidget
|
|
552
|
-
if widget_type == "signature":
|
|
553
|
-
_class = SignatureWidget
|
|
554
|
-
if widget_type == "image":
|
|
555
|
-
_class = ImageWidget
|
|
556
|
-
if _class is None:
|
|
557
|
-
return self
|
|
558
|
-
|
|
559
|
-
obj = _class(name=name, page_number=page_number, x=x, y=y, **kwargs)
|
|
560
|
-
watermarks = obj.watermarks(self.read())
|
|
561
|
-
|
|
562
|
-
self._stream = copy_watermark_widgets(self.read(), watermarks, [name], None)
|
|
563
|
-
hook_params = obj.hook_params
|
|
623
|
+
watermarks = getattr(widget_class, "bulk_watermarks")(widgets, self._read())
|
|
624
|
+
self._stream = copy_watermark_widgets(
|
|
625
|
+
self._read(),
|
|
626
|
+
watermarks,
|
|
627
|
+
[widget.name for widget in widgets],
|
|
628
|
+
None,
|
|
629
|
+
)
|
|
564
630
|
|
|
565
631
|
self._init_helper()
|
|
566
|
-
|
|
567
|
-
|
|
632
|
+
|
|
633
|
+
for widget in widgets:
|
|
634
|
+
for k, v in widget.hook_params:
|
|
635
|
+
self.widgets[widget.name].__setattr__(k, v)
|
|
568
636
|
|
|
569
637
|
return self
|
|
570
638
|
|
|
639
|
+
def create_field(
|
|
640
|
+
self,
|
|
641
|
+
field: FieldTypes,
|
|
642
|
+
) -> PdfWrapper:
|
|
643
|
+
"""
|
|
644
|
+
Creates a new form field (widget) on the PDF using a `FieldTypes` object.
|
|
645
|
+
|
|
646
|
+
This method simplifies widget creation by taking a `FieldTypes` object
|
|
647
|
+
and delegating to the internal `_bulk_create_fields` method.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
field (FieldTypes): An object representing the field to create.
|
|
651
|
+
This object encapsulates all necessary properties like name,
|
|
652
|
+
page number, coordinates, and type of the field.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
return self._bulk_create_fields([field])
|
|
659
|
+
|
|
571
660
|
def update_widget_key(
|
|
572
661
|
self, old_key: str, new_key: str, index: int = 0, defer: bool = False
|
|
573
662
|
) -> PdfWrapper:
|
|
@@ -598,7 +687,7 @@ class PdfWrapper:
|
|
|
598
687
|
|
|
599
688
|
self._key_update_tracker[new_key] = old_key
|
|
600
689
|
self._stream = update_widget_keys(
|
|
601
|
-
self.
|
|
690
|
+
self._read(), self.widgets, [old_key], [new_key], [index]
|
|
602
691
|
)
|
|
603
692
|
self._init_helper()
|
|
604
693
|
|
|
@@ -623,7 +712,7 @@ class PdfWrapper:
|
|
|
623
712
|
indices = [each[2] for each in self._keys_to_update]
|
|
624
713
|
|
|
625
714
|
self._stream = update_widget_keys(
|
|
626
|
-
self.
|
|
715
|
+
self._read(), self.widgets, old_keys, new_keys, indices
|
|
627
716
|
)
|
|
628
717
|
|
|
629
718
|
for each in self._keys_to_update:
|
|
@@ -633,102 +722,29 @@ class PdfWrapper:
|
|
|
633
722
|
|
|
634
723
|
return self
|
|
635
724
|
|
|
636
|
-
def
|
|
637
|
-
self,
|
|
638
|
-
text: str,
|
|
639
|
-
page_number: int,
|
|
640
|
-
x: Union[float, int],
|
|
641
|
-
y: Union[float, int],
|
|
642
|
-
**kwargs,
|
|
643
|
-
) -> PdfWrapper:
|
|
725
|
+
def draw(self, elements: Sequence[RawTypes]) -> PdfWrapper:
|
|
644
726
|
"""
|
|
645
|
-
Draws text
|
|
646
|
-
|
|
647
|
-
Args:
|
|
648
|
-
text (str): The text to draw.
|
|
649
|
-
page_number (int): The page number to draw on.
|
|
650
|
-
x (Union[float, int]): The x coordinate of the text.
|
|
651
|
-
y (Union[float, int]): The y coordinate of the text.
|
|
652
|
-
**kwargs: Additional keyword arguments:
|
|
653
|
-
- `font` (str): The name of the font to use (default: DEFAULT_FONT).
|
|
654
|
-
- `font_size` (float): The font size in points (default: DEFAULT_FONT_SIZE).
|
|
655
|
-
- `font_color` (Tuple[float, float, float]): The font color as an RGB tuple (default: DEFAULT_FONT_COLOR).
|
|
656
|
-
|
|
657
|
-
Returns:
|
|
658
|
-
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
659
|
-
"""
|
|
660
|
-
|
|
661
|
-
new_widget = Text("new")
|
|
662
|
-
new_widget.value = text
|
|
663
|
-
new_widget.font = kwargs.get("font", DEFAULT_FONT)
|
|
664
|
-
new_widget.font_size = kwargs.get("font_size", DEFAULT_FONT_SIZE)
|
|
665
|
-
new_widget.font_color = kwargs.get("font_color", DEFAULT_FONT_COLOR)
|
|
727
|
+
Draws raw elements (text, images, etc.) directly onto the PDF pages.
|
|
666
728
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
"text",
|
|
671
|
-
[
|
|
672
|
-
{
|
|
673
|
-
"widget": new_widget,
|
|
674
|
-
"x": x,
|
|
675
|
-
"y": y,
|
|
676
|
-
}
|
|
677
|
-
],
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
stream_with_widgets = self.read()
|
|
681
|
-
self._stream = merge_watermarks_with_pdf(self.read(), watermarks)
|
|
682
|
-
self._stream = copy_watermark_widgets(
|
|
683
|
-
remove_all_widgets(self.read()), stream_with_widgets, None, None
|
|
684
|
-
)
|
|
685
|
-
# because copy_watermark_widgets and remove_all_widgets
|
|
686
|
-
self._reregister_font()
|
|
687
|
-
|
|
688
|
-
return self
|
|
689
|
-
|
|
690
|
-
def draw_image(
|
|
691
|
-
self,
|
|
692
|
-
image: Union[bytes, str, BinaryIO],
|
|
693
|
-
page_number: int,
|
|
694
|
-
x: Union[float, int],
|
|
695
|
-
y: Union[float, int],
|
|
696
|
-
width: Union[float, int],
|
|
697
|
-
height: Union[float, int],
|
|
698
|
-
rotation: Union[float, int] = 0,
|
|
699
|
-
) -> PdfWrapper:
|
|
700
|
-
"""
|
|
701
|
-
Draws an image on the PDF.
|
|
729
|
+
This method is the primary mechanism for drawing non-form field content.
|
|
730
|
+
It takes a list of `RawText` or `RawImage` objects and renders them
|
|
731
|
+
onto the PDF document as watermarks.
|
|
702
732
|
|
|
703
733
|
Args:
|
|
704
|
-
|
|
705
|
-
- bytes: The raw image data as a byte string.
|
|
706
|
-
- str: The file path to the image.
|
|
707
|
-
- BinaryIO: An open file-like object containing the image data.
|
|
708
|
-
page_number (int): The page number to draw the image on.
|
|
709
|
-
x (Union[float, int]): The x coordinate of the image.
|
|
710
|
-
y (Union[float, int]): The y coordinate of the image.
|
|
711
|
-
width (Union[float, int]): The width of the image.
|
|
712
|
-
height (Union[float, int]): The height of the image.
|
|
713
|
-
rotation (Union[float, int]): The rotation of the image in degrees (default: 0).
|
|
734
|
+
elements (Sequence[RawTypes]): A list of raw elements to draw (e.g., [RawText(...), RawImage(...)]).
|
|
714
735
|
|
|
715
736
|
Returns:
|
|
716
737
|
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
717
738
|
"""
|
|
718
739
|
|
|
719
|
-
image = fp_or_f_obj_or_stream_to_stream(image)
|
|
720
|
-
image = rotate_image(image, rotation)
|
|
721
740
|
watermarks = create_watermarks_and_draw(
|
|
722
|
-
self.
|
|
723
|
-
page_number,
|
|
724
|
-
"image",
|
|
725
|
-
[{"stream": image, "x": x, "y": y, "width": width, "height": height}],
|
|
741
|
+
self._read(), [each.to_draw for each in elements]
|
|
726
742
|
)
|
|
727
743
|
|
|
728
|
-
stream_with_widgets = self.
|
|
729
|
-
self._stream = merge_watermarks_with_pdf(self.
|
|
744
|
+
stream_with_widgets = self._read()
|
|
745
|
+
self._stream = merge_watermarks_with_pdf(self._read(), watermarks)
|
|
730
746
|
self._stream = copy_watermark_widgets(
|
|
731
|
-
remove_all_widgets(self.
|
|
747
|
+
remove_all_widgets(self._read()), stream_with_widgets, None, None
|
|
732
748
|
)
|
|
733
749
|
# because copy_watermark_widgets and remove_all_widgets
|
|
734
750
|
self._reregister_font()
|
|
@@ -751,7 +767,7 @@ class PdfWrapper:
|
|
|
751
767
|
- str: The file path to the TTF file.
|
|
752
768
|
- BinaryIO: An open file-like object containing the TTF file data.
|
|
753
769
|
first_time (bool): Whether this is the first time the font is being registered (default: True).
|
|
754
|
-
If True and `
|
|
770
|
+
If True and `need_appearances` is enabled, a blank text string is drawn to ensure the font is properly embedded in the PDF.
|
|
755
771
|
|
|
756
772
|
Returns:
|
|
757
773
|
PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
|
|
@@ -760,10 +776,10 @@ class PdfWrapper:
|
|
|
760
776
|
ttf_file = fp_or_f_obj_or_stream_to_stream(ttf_file)
|
|
761
777
|
|
|
762
778
|
if register_font(font_name, ttf_file) if ttf_file is not None else False:
|
|
763
|
-
if first_time and getattr(self, "
|
|
764
|
-
self.
|
|
779
|
+
if first_time and getattr(self, "need_appearances"):
|
|
780
|
+
self.draw([RawText(" ", 1, 0, 0, font=font_name)])
|
|
765
781
|
self._stream, new_font_name = register_font_acroform(
|
|
766
|
-
self.
|
|
782
|
+
self._read(), ttf_file, getattr(self, "need_appearances")
|
|
767
783
|
)
|
|
768
784
|
self._available_fonts[font_name] = new_font_name
|
|
769
785
|
self._font_register_events.append((font_name, ttf_file))
|