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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. PyPDFForm/__init__.py +5 -3
  2. PyPDFForm/adapter.py +33 -1
  3. PyPDFForm/ap.py +99 -0
  4. PyPDFForm/assets/__init__.py +0 -0
  5. PyPDFForm/assets/blank.py +100 -0
  6. PyPDFForm/constants.py +20 -2
  7. PyPDFForm/coordinate.py +7 -11
  8. PyPDFForm/deprecation.py +30 -0
  9. PyPDFForm/filler.py +17 -36
  10. PyPDFForm/font.py +16 -16
  11. PyPDFForm/hooks.py +169 -31
  12. PyPDFForm/image.py +0 -3
  13. PyPDFForm/middleware/__init__.py +35 -0
  14. PyPDFForm/middleware/base.py +24 -5
  15. PyPDFForm/middleware/checkbox.py +18 -1
  16. PyPDFForm/middleware/signature.py +0 -1
  17. PyPDFForm/patterns.py +71 -13
  18. PyPDFForm/raw/__init__.py +37 -0
  19. PyPDFForm/raw/circle.py +65 -0
  20. PyPDFForm/raw/ellipse.py +69 -0
  21. PyPDFForm/raw/image.py +79 -0
  22. PyPDFForm/raw/line.py +65 -0
  23. PyPDFForm/raw/rect.py +70 -0
  24. PyPDFForm/raw/text.py +73 -0
  25. PyPDFForm/template.py +114 -10
  26. PyPDFForm/types.py +49 -0
  27. PyPDFForm/utils.py +31 -41
  28. PyPDFForm/watermark.py +153 -44
  29. PyPDFForm/widgets/__init__.py +1 -0
  30. PyPDFForm/widgets/base.py +79 -59
  31. PyPDFForm/widgets/checkbox.py +30 -30
  32. PyPDFForm/widgets/dropdown.py +42 -40
  33. PyPDFForm/widgets/image.py +17 -16
  34. PyPDFForm/widgets/radio.py +27 -28
  35. PyPDFForm/widgets/signature.py +96 -60
  36. PyPDFForm/widgets/text.py +40 -40
  37. PyPDFForm/wrapper.py +256 -240
  38. {pypdfform-3.5.1.dist-info → pypdfform-4.2.0.dist-info}/METADATA +33 -26
  39. pypdfform-4.2.0.dist-info/RECORD +47 -0
  40. {pypdfform-3.5.1.dist-info → pypdfform-4.2.0.dist-info}/licenses/LICENSE +1 -1
  41. pypdfform-3.5.1.dist-info/RECORD +0 -35
  42. /PyPDFForm/{widgets → assets}/bedrock.py +0 -0
  43. {pypdfform-3.5.1.dist-info → pypdfform-4.2.0.dist-info}/WHEEL +0 -0
  44. {pypdfform-3.5.1.dist-info → pypdfform-4.2.0.dist-info}/top_level.txt +0 -0
PyPDFForm/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 typing import TYPE_CHECKING, BinaryIO, Dict, List, Sequence, Tuple, Union
35
- from warnings import warn
36
-
37
- from .adapter import fp_or_f_obj_or_stream_to_stream
38
- from .constants import (DEFAULT_FONT, DEFAULT_FONT_COLOR, DEFAULT_FONT_SIZE,
39
- DEPRECATION_NOTICE, VERSION_IDENTIFIER_PREFIX,
40
- VERSION_IDENTIFIERS)
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 .template import build_widgets, update_widget_keys
51
- from .utils import (enable_adobe_mode, generate_unique_suffix,
52
- get_page_streams, merge_two_pdfs, remove_all_widgets)
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.checkbox import CheckBoxWidget
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
- - `adobe_mode` (bool): Whether to enable Adobe-specific compatibility mode.
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
- ("adobe_mode", False),
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 `adobe_mode=False`.
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 two PDF wrappers together, creating a new `PdfWrapper` containing the combined content.
125
+ Merges PDF wrappers together, creating a new `PdfWrapper` containing the combined content.
128
126
 
129
- This method allows you to combine two PDF forms into a single form. It handles potential
130
- naming conflicts between form fields by adding a unique suffix to the field names in the second form.
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 to merge with.
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 not self.read():
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.read():
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
- merge_two_pdfs(self.read(), other.read()),
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.read(),
181
+ self._read(),
176
182
  getattr(self, "use_full_widget_name"),
177
183
  )
178
- if self.read()
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.read():
199
- self._available_fonts.update(**get_all_available_fonts(self.read()))
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.read().startswith(each):
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 sequence of `PdfWrapper` objects, each representing a single page in the PDF document.
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 sequence of `PdfWrapper` objects, one for each page in the PDF.
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.read(), None, i),
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.read())))
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 content from the underlying stream.
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 returns the current state of the PDF as a byte string.
325
- It also triggers any pending widget hooks and applies Adobe mode if enabled.
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 content as bytes.
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, path: str) -> PdfWrapper:
423
+ def write(self, dest: Union[str, BinaryIO]) -> PdfWrapper:
354
424
  """
355
- Writes the PDF content to a file.
425
+ Writes the PDF to a file.
356
426
 
357
427
  Args:
358
- path (str): The file path to write the PDF to.
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
- with open(path, "wb+") as f:
365
- f.write(self.read())
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.read().replace(
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.read()
476
+ stream_with_widgets = self._read()
403
477
  self._stream = copy_watermark_widgets(
404
478
  generate_coordinate_grid(
405
- remove_all_widgets(self.read()),
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 are form field names
428
- and values are the data to fill the fields with. Values can be strings, booleans, or integers.
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.read(),
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 create_field(
466
- self,
467
- field: FieldTypes,
468
- ) -> PdfWrapper:
541
+ def bulk_create_fields(self, fields: Sequence[FieldTypes]) -> PdfWrapper:
469
542
  """
470
- Creates a new form field (widget) on the PDF using a `FieldTypes` object.
543
+ Creates multiple new form fields (widgets) on the PDF in a single operation.
471
544
 
472
- This method simplifies widget creation by taking a `FieldTypes` object,
473
- extracting its properties, and then delegating to the `create_widget` method.
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
- field (FieldTypes): An object representing the field to create.
477
- This object encapsulates all necessary properties like name,
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
- field_dict = asdict(field)
485
- widget_type = field_dict.pop("_field_type")
486
- name = field_dict.pop("name")
487
- page_number = field_dict.pop("page_number")
488
- x = field_dict.pop("x")
489
- y = field_dict.pop("y")
490
-
491
- field_dict["suppress_deprecation_notice"] = True
492
- return self.create_widget(
493
- widget_type,
494
- name,
495
- page_number,
496
- x,
497
- y,
498
- **{k: v for k, v in field_dict.items() if v is not None},
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
- def create_widget(
502
- self,
503
- widget_type: str,
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
- Creates a new form field (widget) on the PDF.
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
- widget_type (str): The type of widget to create. Valid values are:
515
- - "text": A text field.
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
- if not kwargs.get("suppress_deprecation_notice"):
534
- warn(
535
- DEPRECATION_NOTICE.format(
536
- f"{self.__class__.__name__}.create_widget()",
537
- f"{self.__class__.__name__}.create_field()",
538
- ),
539
- DeprecationWarning, # noqa: PT030
540
- stacklevel=2,
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
- _class = None
544
- if widget_type == "text":
545
- _class = TextWidget
546
- if widget_type == "checkbox":
547
- _class = CheckBoxWidget
548
- if widget_type == "dropdown":
549
- _class = DropdownWidget
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
- for k, v in hook_params:
567
- self.widgets[name].__setattr__(k, v)
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.read(), self.widgets, [old_key], [new_key], [index]
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.read(), self.widgets, old_keys, new_keys, indices
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 draw_text(
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 on the PDF.
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
- watermarks = create_watermarks_and_draw(
668
- self.read(),
669
- page_number,
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
- image (Union[bytes, str, BinaryIO]): The image data, provided as either:
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.read(),
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.read()
729
- self._stream = merge_watermarks_with_pdf(self.read(), watermarks)
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.read()), stream_with_widgets, None, None
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 `adobe_mode` is enabled, a blank text string is drawn to ensure the font is properly embedded in the PDF.
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, "adobe_mode"):
764
- self.draw_text(" ", 1, 0, 0, font=font_name)
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.read(), ttf_file, getattr(self, "adobe_mode")
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))