PyPDFForm 2.4.0__py3-none-any.whl → 3.0.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.

Potentially problematic release.


This version of PyPDFForm might be problematic. Click here for more details.

PyPDFForm/wrapper.py CHANGED
@@ -1,42 +1,43 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Provides high-level wrapper classes for working with PDF forms.
3
-
4
- This module contains the FormWrapper and PdfWrapper classes which provide
5
- a user-friendly interface for:
6
- - Filling PDF form fields
7
- - Creating and modifying PDF form widgets
8
- - Drawing text and images on PDFs
9
- - Merging PDF documents
10
- - Generating coordinate grids
11
- - Other PDF manipulation tasks
12
-
13
- The wrappers handle low-level PDF operations while exposing simple methods
14
- for common use cases.
2
+ """
3
+ A module for wrapping PDF form operations, providing a high-level interface
4
+ for filling, creating, and manipulating PDF forms.
5
+
6
+ This module simplifies common tasks such as:
7
+ - Filling PDF forms with data from a dictionary.
8
+ - Creating new form fields (widgets) on a PDF.
9
+ - Drawing text and images onto a PDF.
10
+ - Registering custom fonts for use in form fields.
11
+ - Merging multiple PDF forms.
12
+
13
+ The core class, `PdfWrapper`, encapsulates a PDF document and provides
14
+ methods for interacting with its form fields and content. It leverages
15
+ lower-level modules within the `PyPDFForm` library to handle the
16
+ underlying PDF manipulation.
15
17
  """
16
18
 
17
19
  from __future__ import annotations
18
20
 
19
21
  from functools import cached_property
20
- from typing import BinaryIO, Dict, List, Tuple, Union
22
+ from typing import BinaryIO, Dict, List, Sequence, Tuple, Union
21
23
 
22
24
  from .adapter import fp_or_f_obj_or_stream_to_stream
23
25
  from .constants import (DEFAULT_FONT, DEFAULT_FONT_COLOR, DEFAULT_FONT_SIZE,
24
- NEW_LINE_SYMBOL, VERSION_IDENTIFIER_PREFIX,
25
- VERSION_IDENTIFIERS)
26
+ VERSION_IDENTIFIER_PREFIX, VERSION_IDENTIFIERS)
26
27
  from .coordinate import generate_coordinate_grid
27
- from .filler import fill, simple_fill
28
- from .font import register_font
28
+ from .filler import fill
29
+ from .font import (get_all_available_fonts, register_font,
30
+ register_font_acroform)
31
+ from .hooks import trigger_widget_hooks
29
32
  from .image import rotate_image
30
33
  from .middleware.dropdown import Dropdown
34
+ from .middleware.signature import Signature
31
35
  from .middleware.text import Text
32
- from .template import (build_widgets, dropdown_to_text,
33
- set_character_x_paddings, update_text_field_attributes,
34
- update_widget_keys)
35
- from .utils import (generate_unique_suffix, get_page_streams, merge_two_pdfs,
36
- preview_widget_to_draw, remove_all_widgets)
36
+ from .template import build_widgets, update_widget_keys
37
+ from .utils import (enable_adobe_mode, generate_unique_suffix,
38
+ get_page_streams, merge_two_pdfs, remove_all_widgets)
37
39
  from .watermark import (copy_watermark_widgets, create_watermarks_and_draw,
38
40
  merge_watermarks_with_pdf)
39
- from .widgets.base import handle_non_acro_form_params
40
41
  from .widgets.checkbox import CheckBoxWidget
41
42
  from .widgets.dropdown import DropdownWidget
42
43
  from .widgets.image import ImageWidget
@@ -45,383 +46,339 @@ from .widgets.signature import SignatureWidget
45
46
  from .widgets.text import TextWidget
46
47
 
47
48
 
48
- class FormWrapper:
49
- """Base class providing core PDF form filling functionality.
49
+ class PdfWrapper:
50
+ """
51
+ A class to wrap PDF form operations, providing a simplified interface
52
+ for common tasks such as filling, creating, and manipulating PDF forms.
50
53
 
51
- This wrapper handles basic PDF form operations:
52
- - Accessing raw PDF data through the read() method
53
- - Filling existing form fields with provided values
54
+ The `PdfWrapper` class encapsulates a PDF document and provides methods
55
+ for interacting with its form fields (widgets) and content. It leverages
56
+ lower-level modules within the `PyPDFForm` library to handle the
57
+ underlying PDF manipulation.
54
58
 
55
- Note: This class does not parse or analyze form fields - it only fills values
56
- into fields that already exist in the template PDF.
59
+ Attributes:
60
+ USER_PARAMS (list): A list of user-configurable parameters and their default values.
61
+ These parameters can be set during initialization using keyword arguments.
62
+ Current parameters include:
63
+ - `use_full_widget_name` (bool): Whether to use the full widget name when filling the form.
64
+ - `adobe_mode` (bool): Whether to enable Adobe-specific compatibility mode.
57
65
 
58
- The FormWrapper is designed to be extended by PdfWrapper which adds
59
- more advanced features like form analysis and widget creation.
60
66
  """
61
67
 
68
+ USER_PARAMS = [
69
+ ("use_full_widget_name", False),
70
+ ("adobe_mode", False),
71
+ ]
72
+
62
73
  def __init__(
63
74
  self,
64
75
  template: Union[bytes, str, BinaryIO] = b"",
65
76
  **kwargs,
66
77
  ) -> None:
67
- """Initializes the base form wrapper with a PDF template.
68
-
69
- Args:
70
- template: PDF form as bytes, file path, or file object. Defaults to
71
- empty bytes if not provided.
72
- **kwargs: Additional options:
73
- use_full_widget_name: If True, uses complete widget names including
74
- field hierarchy (default: False)
78
+ """
79
+ Constructor method for the `PdfWrapper` class.
75
80
 
76
- Initializes:
77
- - Internal PDF stream from the template
78
- - Basic form filling capabilities
79
- - Widget naming configuration from kwargs
81
+ Initializes a new `PdfWrapper` object with the given template PDF and optional keyword arguments.
80
82
 
81
- Note:
82
- This base class is designed to be extended by PdfWrapper which adds
83
- more advanced features. For most use cases, you'll want to use PdfWrapper.
83
+ Args:
84
+ template (Union[bytes, str, BinaryIO]): The template PDF, provided as either:
85
+ - bytes: The raw PDF data as a byte string.
86
+ - str: The file path to the PDF.
87
+ - BinaryIO: An open file-like object containing the PDF data.
88
+ Defaults to an empty byte string (b""), which creates a blank PDF.
89
+ **kwargs: Additional keyword arguments to configure the `PdfWrapper`.
90
+ These arguments are used to set the user-configurable parameters defined in `USER_PARAMS`.
91
+ For example: `use_full_widget_name=True` or `adobe_mode=False`.
84
92
  """
85
93
 
86
94
  super().__init__()
87
- self.stream = fp_or_f_obj_or_stream_to_stream(template)
88
- self.use_full_widget_name = kwargs.get("use_full_widget_name", False)
95
+ self._stream = fp_or_f_obj_or_stream_to_stream(template)
96
+ self.widgets = {}
97
+ self._available_fonts = {} # for setting /F1
98
+ self._font_register_events = [] # for reregister
99
+ self._key_update_tracker = {} # for update key preserve old key attrs
100
+ self._keys_to_update = [] # for bulk update keys
89
101
 
90
- def read(self) -> bytes:
91
- """Returns the raw bytes of the PDF form data.
102
+ # sets attrs from kwargs
103
+ for attr, default in self.USER_PARAMS:
104
+ setattr(self, attr, kwargs.get(attr, default))
92
105
 
93
- This method provides access to the underlying PDF bytes after operations
94
- like fill() have been performed. No parsing or analysis of the PDF
95
- content is done - the bytes are returned as-is.
106
+ self._init_helper()
96
107
 
97
- Returns:
98
- bytes: The complete PDF document as a byte string
108
+ def __add__(self, other: PdfWrapper) -> PdfWrapper:
99
109
  """
110
+ Merges two PDF wrappers together, creating a new `PdfWrapper` containing the combined content.
100
111
 
101
- return self.stream
102
-
103
- def fill(
104
- self,
105
- data: Dict[str, Union[str, bool, int]],
106
- **kwargs,
107
- ) -> FormWrapper:
108
- """Fills form fields in the PDF with provided values.
109
-
110
- Takes a dictionary of field names to values and updates the corresponding
111
- form fields in the PDF. Supports these value types:
112
- - Strings for text fields
113
- - Booleans for checkboxes (True=checked, False=unchecked)
114
- - Integers for numeric fields and dropdown selections
115
-
116
- Only fields that exist in the template PDF will be filled - unknown field
117
- names are silently ignored.
112
+ This method allows you to combine two PDF forms into a single form. It handles potential
113
+ naming conflicts between form fields by adding a unique suffix to the field names in the second form.
118
114
 
119
115
  Args:
120
- data: Dictionary mapping field names to values. Supported types:
121
- str: For text fields
122
- bool: For checkboxes (True=checked)
123
- int: For numeric fields and dropdown selections
124
- **kwargs: Additional options:
125
- flatten (bool): If True, makes form fields read-only after filling
126
- (default: False)
127
- adobe_mode (bool): If True, uses Adobe-compatible filling logic
128
- (default: False)
116
+ other (PdfWrapper): The other `PdfWrapper` object to merge with.
129
117
 
130
118
  Returns:
131
- FormWrapper: Returns self to allow method chaining
119
+ PdfWrapper: A new `PdfWrapper` object containing the merged PDFs.
132
120
  """
133
121
 
134
- widgets = (
135
- build_widgets(self.stream, self.use_full_widget_name, False)
136
- if self.stream
137
- else {}
138
- )
139
-
140
- for key, value in data.items():
141
- if key in widgets:
142
- widgets[key].value = value
143
-
144
- self.stream = simple_fill(
145
- self.read(),
146
- widgets,
147
- use_full_widget_name=self.use_full_widget_name,
148
- flatten=kwargs.get("flatten", False),
149
- adobe_mode=kwargs.get("adobe_mode", False),
150
- )
151
-
152
- return self
153
-
154
-
155
- class PdfWrapper(FormWrapper):
156
- """Extended PDF form wrapper with advanced features.
157
-
158
- Inherits from FormWrapper and adds capabilities for:
159
- - Creating and modifying form widgets
160
- - Drawing text and images
161
- - Merging PDF documents
162
- - Generating coordinate grids
163
- - Form schema generation
164
- - Font registration
165
-
166
- Key Features:
167
- - Maintains widget state and properties
168
- - Supports per-page operations
169
- - Handles PDF version management
170
- - Provides preview functionality
171
- """
172
-
173
- USER_PARAMS = [
174
- ("global_font", None),
175
- ("global_font_size", None),
176
- ("global_font_color", None),
177
- ("use_full_widget_name", False),
178
- ("render_widgets", True),
179
- ]
122
+ if not self.read():
123
+ return other
180
124
 
181
- def __init__(
182
- self,
183
- template: Union[bytes, str, BinaryIO] = b"",
184
- **kwargs,
185
- ) -> None:
186
- """Initializes the PDF wrapper with template and configuration.
125
+ if not other.read():
126
+ return self
187
127
 
188
- Args:
189
- template: PDF form as bytes, file path, or file object. Defaults to
190
- empty bytes if not provided.
191
- **kwargs: Optional configuration parameters including:
192
- global_font: Default font name for text fields
193
- global_font_size: Default font size
194
- global_font_color: Default font color as RGB tuple
195
- use_full_widget_name: Whether to use full widget names
196
- render_widgets: Whether to render widgets in the PDF
197
-
198
- Initializes:
199
- - Widgets dictionary to track form fields
200
- - Keys update queue for deferred operations
201
- - Any specified global settings from kwargs
202
- """
203
-
204
- super().__init__(template)
205
- self.widgets = {}
206
- self._keys_to_update = []
128
+ unique_suffix = generate_unique_suffix()
129
+ for k in self.widgets:
130
+ if k in other.widgets:
131
+ other.update_widget_key(k, f"{k}-{unique_suffix}", defer=True)
207
132
 
208
- for attr, default in self.USER_PARAMS:
209
- setattr(self, attr, kwargs.get(attr, default))
133
+ other.commit_widget_key_updates()
210
134
 
211
- self._init_helper()
135
+ # user params are based on the first object
136
+ result = self.__class__(
137
+ merge_two_pdfs(self.read(), other.read()),
138
+ **{each[0]: getattr(self, each[0], each[1]) for each in self.USER_PARAMS},
139
+ )
212
140
 
213
- def _init_helper(self, key_to_refresh: str = None) -> None:
214
- """Internal method to refresh widget state after PDF stream changes.
141
+ # inherit fonts
142
+ for event in self._font_register_events:
143
+ result.register_font(event[0], event[1])
215
144
 
216
- Called whenever the underlying PDF stream is modified to:
217
- - Rebuild the widgets dictionary
218
- - Preserve existing widget properties
219
- - Apply global font settings to text widgets
220
- - Handle special refresh cases for specific widgets
145
+ return result
221
146
 
222
- Args:
223
- key_to_refresh: Optional specific widget key that needs refreshing.
224
- If provided, only that widget's font properties will be updated.
225
- If None, all text widgets will have their fonts updated.
147
+ def _init_helper(self) -> None:
148
+ """
149
+ Helper method to initialize widgets and available fonts.
226
150
 
227
- Note:
228
- This is an internal method and typically shouldn't be called directly.
229
- It's automatically invoked after operations that modify the PDF stream.
151
+ This method is called during initialization and after certain operations
152
+ that modify the PDF content (e.g., filling, creating widgets, updating keys).
153
+ It rebuilds the widget dictionary and updates the available fonts.
230
154
  """
231
155
 
232
- refresh_not_needed = {}
233
156
  new_widgets = (
234
157
  build_widgets(
235
158
  self.read(),
236
159
  getattr(self, "use_full_widget_name"),
237
- getattr(self, "render_widgets"),
238
160
  )
239
161
  if self.read()
240
162
  else {}
241
163
  )
164
+ # ensure old widgets don't get overwritten
242
165
  for k, v in self.widgets.items():
243
166
  if k in new_widgets:
244
167
  new_widgets[k] = v
245
- refresh_not_needed[k] = True
168
+
169
+ # update key preserve old key attrs
170
+ for k, v in new_widgets.items():
171
+ if k in self._key_update_tracker:
172
+ for name, value in self.widgets[
173
+ self._key_update_tracker[k]
174
+ ].__dict__.items():
175
+ if not name.startswith("_"):
176
+ setattr(v, name, value)
177
+ self._key_update_tracker = {}
178
+
246
179
  self.widgets = new_widgets
247
180
 
248
- for key, value in self.widgets.items():
249
- if (key_to_refresh and key == key_to_refresh) or (
250
- key_to_refresh is None
251
- and isinstance(value, Text)
252
- and not refresh_not_needed.get(key)
253
- ):
254
- value.font = getattr(self, "global_font")
255
- value.font_size = getattr(self, "global_font_size")
256
- value.font_color = getattr(self, "global_font_color")
181
+ if self.read():
182
+ self._available_fonts.update(**get_all_available_fonts(self.read()))
183
+
184
+ def _reregister_font(self) -> PdfWrapper:
185
+ """
186
+ Reregisters fonts after PDF content modifications.
187
+
188
+ This method is called after operations that modify the PDF content
189
+ (e.g., drawing text, drawing images) to ensure that custom fonts
190
+ are correctly registered and available for use.
191
+ """
192
+
193
+ font_register_events_len = len(self._font_register_events)
194
+ for i in range(font_register_events_len):
195
+ event = self._font_register_events[i]
196
+ self.register_font(event[0], event[1], False)
197
+ self._font_register_events = self._font_register_events[
198
+ font_register_events_len:
199
+ ]
200
+
201
+ return self
202
+
203
+ @property
204
+ def schema(self) -> dict:
205
+ """
206
+ Returns the JSON schema of the PDF form, describing the structure and data types of the form fields.
207
+
208
+ This schema can be used to generate user interfaces or validate data before filling the form.
209
+
210
+ Returns:
211
+ dict: A dictionary representing the JSON schema of the PDF form.
212
+ """
213
+
214
+ return {
215
+ "type": "object",
216
+ "properties": {
217
+ key: value.schema_definition for key, value in self.widgets.items()
218
+ },
219
+ }
257
220
 
258
221
  @property
259
222
  def sample_data(self) -> dict:
260
- """Generates a dictionary of sample values for all form fields.
223
+ """
224
+ Returns sample data for the PDF form, providing example values for each form field.
261
225
 
262
- Returns a dictionary mapping each widget/field name to an appropriate
263
- sample value based on its type:
264
- - Text fields: Field name (truncated if max_length specified)
265
- - Checkboxes: True
266
- - Dropdowns: Index of last available choice
267
- - Other fields: Type-specific sample values
226
+ This sample data can be used for testing or demonstration purposes.
268
227
 
269
228
  Returns:
270
- dict: Field names mapped to their sample values
229
+ dict: A dictionary containing sample data for the PDF form.
271
230
  """
272
231
 
273
232
  return {key: value.sample_value for key, value in self.widgets.items()}
274
233
 
275
234
  @property
276
235
  def version(self) -> Union[str, None]:
277
- """Gets the PDF version number from the document header.
278
-
279
- The version is extracted from the PDF header which contains a version
280
- identifier like '%PDF-1.4'. This method returns just the version number
281
- portion (e.g. '1.4') if found, or None if no valid version identifier
282
- is present.
236
+ """
237
+ Returns the PDF version of the underlying PDF document.
283
238
 
284
239
  Returns:
285
- str: The PDF version number (e.g. '1.4') if found
286
- None: If no valid version identifier exists in the PDF
240
+ Union[str, None]: The PDF version as a string, or None if the version cannot be determined.
287
241
  """
288
242
 
289
243
  for each in VERSION_IDENTIFIERS:
290
- if self.stream.startswith(each):
244
+ if self.read().startswith(each):
291
245
  return each.replace(VERSION_IDENTIFIER_PREFIX, b"").decode()
292
246
 
293
247
  return None
294
248
 
295
- @cached_property
296
- def pages(self) -> List[PdfWrapper]:
297
- """Returns individual page wrappers for each page in the PDF.
249
+ @property
250
+ def fonts(self) -> list:
251
+ """
252
+ Returns a list of the names of the currently registered fonts.
298
253
 
299
- Creates a separate PdfWrapper instance for each page, maintaining all
300
- the original wrapper's settings (fonts, rendering options etc.). This
301
- allows per-page operations while preserving the parent's configuration.
254
+ Returns:
255
+ list: A list of font names (str).
256
+ """
257
+
258
+ return list(self._available_fonts.keys())
259
+
260
+ @cached_property
261
+ def pages(self) -> Sequence[PdfWrapper]:
262
+ """
263
+ Returns a sequence of `PdfWrapper` objects, each representing a single page in the PDF document.
302
264
 
303
- The result is cached after first access for better performance with
304
- repeated calls.
265
+ This allows you to work with individual pages of the PDF, for example, to extract text or images from a specific page.
305
266
 
306
267
  Returns:
307
- List[PdfWrapper]: List of wrapper objects, one per page
268
+ Sequence[PdfWrapper]: A sequence of `PdfWrapper` objects, one for each page in the PDF.
308
269
  """
309
270
 
310
- return [
271
+ result = [
311
272
  self.__class__(
312
- copy_watermark_widgets(each, self.stream, None, i),
273
+ copy_watermark_widgets(each, self.read(), None, i),
313
274
  **{param: getattr(self, param) for param, _ in self.USER_PARAMS},
314
275
  )
315
276
  for i, each in enumerate(get_page_streams(remove_all_widgets(self.read())))
316
277
  ]
317
278
 
318
- def change_version(self, version: str) -> PdfWrapper:
319
- """Changes the PDF version identifier in the document header.
279
+ # because copy_watermark_widgets and remove_all_widgets
280
+ if self._font_register_events:
281
+ for event in self._font_register_events:
282
+ for page in result:
283
+ page.register_font(event[0], event[1])
320
284
 
321
- Modifies the version header (e.g. '%PDF-1.4') to match the specified version.
322
- Note this only changes the version identifier, not the actual PDF features used.
285
+ return result
323
286
 
324
- Args:
325
- version: Target version string (e.g. '1.4', '1.7')
287
+ def read(self) -> bytes:
288
+ """
289
+ Reads the PDF content from the underlying stream.
290
+
291
+ This method returns the current state of the PDF as a byte string.
292
+ It also triggers any pending widget hooks and applies Adobe mode if enabled.
326
293
 
327
294
  Returns:
328
- PdfWrapper: Returns self to allow method chaining
295
+ bytes: The PDF content as bytes.
329
296
  """
330
297
 
331
- self.stream = self.stream.replace(
332
- VERSION_IDENTIFIER_PREFIX + bytes(self.version, "utf-8"),
333
- VERSION_IDENTIFIER_PREFIX + bytes(version, "utf-8"),
334
- 1,
335
- )
298
+ if any(widget.hooks_to_trigger for widget in self.widgets.values()):
299
+ for widget in self.widgets.values():
300
+ if (
301
+ isinstance(widget, (Text, Dropdown))
302
+ and widget.font not in self._available_fonts.values()
303
+ and widget.font in self._available_fonts
304
+ ):
305
+ widget.font = self._available_fonts.get(
306
+ widget.font
307
+ ) # from `new_font` to `/F1`
308
+
309
+ self._stream = trigger_widget_hooks(
310
+ self._stream,
311
+ self.widgets,
312
+ getattr(self, "use_full_widget_name"),
313
+ )
336
314
 
337
- return self
315
+ if getattr(self, "adobe_mode") and self._stream:
316
+ self._stream = enable_adobe_mode(self._stream) # cached
338
317
 
339
- def __add__(self, other: PdfWrapper) -> PdfWrapper:
340
- """Merges two PDF forms together using the + operator.
318
+ return self._stream
341
319
 
342
- Combines the content of both PDF forms while:
343
- - Preserving each form's widgets and data
344
- - Adding unique suffixes to duplicate field names
345
- - Maintaining all page content and ordering
320
+ def write(self, path: str) -> PdfWrapper:
321
+ """
322
+ Writes the PDF content to a file.
346
323
 
347
324
  Args:
348
- other: Another PdfWrapper instance to merge with
325
+ path (str): The file path to write the PDF to.
349
326
 
350
327
  Returns:
351
- PdfWrapper: New wrapper containing merged PDF
328
+ PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
352
329
  """
353
330
 
354
- if not self.stream:
355
- return other
356
-
357
- if not other.stream:
358
- return self
359
-
360
- unique_suffix = generate_unique_suffix()
361
- for k in self.widgets:
362
- if k in other.widgets:
363
- other.update_widget_key(k, f"{k}-{unique_suffix}", defer=True)
364
-
365
- other.commit_widget_key_updates()
331
+ with open(path, "wb+") as f:
332
+ f.write(self.read())
366
333
 
367
- return self.__class__(merge_two_pdfs(self.stream, other.stream))
334
+ return self
368
335
 
369
- @property
370
- def preview(self) -> bytes:
371
- """Generates a preview PDF showing widget names above their locations.
336
+ def change_version(self, version: str) -> PdfWrapper:
337
+ """
338
+ Changes the PDF version of the underlying document.
372
339
 
373
- Creates a modified version of the PDF where:
374
- - All form widgets are removed
375
- - Widget names are drawn slightly above their original positions
376
- - Helps visualize form field locations without interactive widgets
340
+ Args:
341
+ version (str): The new PDF version string (e.g., "1.7").
377
342
 
378
343
  Returns:
379
- bytes: PDF bytes containing the preview annotations
344
+ PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
380
345
  """
381
346
 
382
- return remove_all_widgets(
383
- fill(
384
- self.stream,
385
- {
386
- key: preview_widget_to_draw(key, value, True)
387
- for key, value in self.widgets.items()
388
- },
389
- getattr(self, "use_full_widget_name"),
390
- )
347
+ self._stream = self.read().replace(
348
+ VERSION_IDENTIFIER_PREFIX + bytes(self.version, "utf-8"),
349
+ VERSION_IDENTIFIER_PREFIX + bytes(version, "utf-8"),
350
+ 1,
391
351
  )
392
352
 
353
+ return self
354
+
393
355
  def generate_coordinate_grid(
394
356
  self, color: Tuple[float, float, float] = (1, 0, 0), margin: float = 100
395
357
  ) -> PdfWrapper:
396
- """Generates a coordinate grid overlay for the PDF.
397
-
398
- Creates a visual grid showing x,y coordinates to help with:
399
- - Precise widget placement
400
- - Measuring distances between elements
401
- - Debugging layout issues
358
+ """
359
+ Generates a coordinate grid on the PDF, useful for debugging layout issues.
402
360
 
403
361
  Args:
404
- color: RGB tuple (0-1 range) for grid line color (default: red)
405
- margin: Spacing between grid lines in PDF units (default: 100)
362
+ color (Tuple[float, float, float]): The color of the grid lines, specified as an RGB tuple (default: red).
363
+ margin (float): The margin around the grid, in points (default: 100).
406
364
 
407
365
  Returns:
408
- PdfWrapper: Returns self to allow method chaining
409
- """
410
-
411
- self.stream = generate_coordinate_grid(
412
- remove_all_widgets(
413
- fill(
414
- self.stream,
415
- {
416
- key: preview_widget_to_draw(key, value, False)
417
- for key, value in self.widgets.items()
418
- },
419
- getattr(self, "use_full_widget_name"),
420
- )
366
+ PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
367
+ """
368
+
369
+ stream_with_widgets = self.read()
370
+ self._stream = copy_watermark_widgets(
371
+ generate_coordinate_grid(
372
+ remove_all_widgets(self.read()),
373
+ color,
374
+ margin,
421
375
  ),
422
- color,
423
- margin,
376
+ stream_with_widgets,
377
+ None,
378
+ None,
424
379
  )
380
+ # because copy_watermark_widgets and remove_all_widgets
381
+ self._reregister_font()
425
382
 
426
383
  return self
427
384
 
@@ -430,40 +387,45 @@ class PdfWrapper(FormWrapper):
430
387
  data: Dict[str, Union[str, bool, int]],
431
388
  **kwargs,
432
389
  ) -> PdfWrapper:
433
- """Fills form fields while preserving widget properties and positions.
434
-
435
- Extends FormWrapper.fill() with additional features:
436
- - Maintains widget properties like fonts and styles
437
- - Converts dropdowns to text fields while preserving choices
438
- - Updates text field attributes and character spacing
390
+ """
391
+ Fills the PDF form with data from a dictionary.
439
392
 
440
393
  Args:
441
- data: Dictionary mapping field names to values (str, bool or int)
442
- **kwargs: Currently unused, maintained for future compatibility
394
+ data (Dict[str, Union[str, bool, int]]): A dictionary where keys are form field names
395
+ and values are the data to fill the fields with. Values can be strings, booleans, or integers.
396
+ **kwargs: Additional keyword arguments:
397
+ - `flatten` (bool): Whether to flatten the form after filling, making the fields read-only (default: False).
443
398
 
444
399
  Returns:
445
- PdfWrapper: Returns self to allow method chaining
400
+ PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
446
401
  """
447
402
 
448
403
  for key, value in data.items():
449
404
  if key in self.widgets:
450
405
  self.widgets[key].value = value
451
406
 
452
- for key, value in self.widgets.items():
453
- if isinstance(value, Dropdown):
454
- self.widgets[key] = dropdown_to_text(value)
455
-
456
- update_text_field_attributes(
457
- self.stream, self.widgets, getattr(self, "use_full_widget_name")
407
+ filled_stream, image_drawn_stream = fill(
408
+ self.read(),
409
+ self.widgets,
410
+ use_full_widget_name=getattr(self, "use_full_widget_name"),
411
+ flatten=kwargs.get("flatten", False),
458
412
  )
459
- if self.read():
460
- self.widgets = set_character_x_paddings(
461
- self.stream, self.widgets, getattr(self, "use_full_widget_name")
413
+
414
+ if image_drawn_stream is not None:
415
+ keys_to_copy = [
416
+ k for k, v in self.widgets.items() if not isinstance(v, Signature)
417
+ ] # only copy non-image fields
418
+ filled_stream = copy_watermark_widgets(
419
+ remove_all_widgets(image_drawn_stream),
420
+ filled_stream,
421
+ keys_to_copy,
422
+ None,
462
423
  )
463
424
 
464
- self.stream = remove_all_widgets(
465
- fill(self.stream, self.widgets, getattr(self, "use_full_widget_name"))
466
- )
425
+ self._stream = filled_stream
426
+ if image_drawn_stream is not None:
427
+ # because copy_watermark_widgets and remove_all_widgets
428
+ self._reregister_font()
467
429
 
468
430
  return self
469
431
 
@@ -477,35 +439,26 @@ class PdfWrapper(FormWrapper):
477
439
  **kwargs,
478
440
  ) -> PdfWrapper:
479
441
  """
480
- Creates a new interactive widget (form field) on the PDF.
481
-
482
- Supported widget types:
483
- - "text": Text input field
484
- - "checkbox": Checkbox field
485
- - "dropdown": Dropdown/combobox field
486
- - "radio": Radio button field
487
- - "signature": Signature field
488
- - "image": Image field
442
+ Creates a new form field (widget) on the PDF.
489
443
 
490
444
  Args:
491
- widget_type (str): Type of widget to create. Must be one of:
492
- "text", "checkbox", "dropdown", "radio", "signature", or "image".
493
- name (str): Unique name/identifier for the widget.
494
- page_number (int): 1-based page number to add the widget to.
495
- x (float or List[float]): X coordinate(s) for widget position.
496
- y (float or List[float]): Y coordinate(s) for widget position.
497
- **kwargs: Additional widget-specific parameters:
498
- For text fields: width, height, font, font_size, etc.
499
- For checkboxes: size, checked, etc.
500
- For dropdowns: choices, default_index, etc.
501
- For signature/image: width, height, etc.
445
+ widget_type (str): The type of widget to create. Valid values are:
446
+ - "text": A text field.
447
+ - "checkbox": A checkbox field.
448
+ - "dropdown": A dropdown field.
449
+ - "radio": A radio button field.
450
+ - "signature": A signature field.
451
+ - "image": An image field.
452
+ name (str): The name of the widget. This name will be used to identify the widget when filling the form.
453
+ page_number (int): The page number to create the widget on (1-based).
454
+ x (Union[float, List[float]]): The x coordinate(s) of the widget.
455
+ If a list is provided, it specifies the x coordinates of multiple instances of the widget.
456
+ y (Union[float, List[float]]): The y coordinate(s) of the widget.
457
+ If a list is provided, it specifies the y coordinates of multiple instances of the widget.
458
+ **kwargs: Additional keyword arguments specific to the widget type.
502
459
 
503
460
  Returns:
504
- PdfWrapper: Returns self to allow method chaining.
505
-
506
- Notes:
507
- - If an unsupported widget_type is provided, the method returns self unchanged.
508
- - After widget creation, the internal widget state is refreshed.
461
+ PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
509
462
  """
510
463
 
511
464
  _class = None
@@ -527,39 +480,34 @@ class PdfWrapper(FormWrapper):
527
480
  obj = _class(name=name, page_number=page_number, x=x, y=y, **kwargs)
528
481
  watermarks = obj.watermarks(self.read())
529
482
 
530
- self.stream = copy_watermark_widgets(self.read(), watermarks, [name], None)
531
- if obj.non_acro_form_params:
532
- self.stream = handle_non_acro_form_params(
533
- self.stream, name, obj.non_acro_form_params
534
- )
535
-
536
- key_to_refresh = ""
537
- if widget_type in ("text", "dropdown"):
538
- key_to_refresh = name
483
+ self._stream = copy_watermark_widgets(self.read(), watermarks, [name], None)
484
+ hook_params = obj.hook_params
539
485
 
540
- self._init_helper(key_to_refresh)
486
+ self._init_helper()
487
+ for k, v in hook_params:
488
+ self.widgets[name].__setattr__(k, v)
541
489
 
542
490
  return self
543
491
 
544
492
  def update_widget_key(
545
493
  self, old_key: str, new_key: str, index: int = 0, defer: bool = False
546
494
  ) -> PdfWrapper:
547
- """Updates the field name/key of an existing widget in the PDF form.
495
+ """
496
+ Updates the key (name) of a widget, allowing you to rename form fields.
548
497
 
549
- Allows renaming form fields while preserving all other properties.
550
- Supports both immediate and deferred (batched) updates.
498
+ This method allows you to change the name of a form field in the PDF. This can be useful for
499
+ standardizing field names or resolving naming conflicts. The update can be performed immediately
500
+ or deferred until `commit_widget_key_updates` is called.
551
501
 
552
502
  Args:
553
- old_key: Current field name/key to be updated
554
- new_key: New field name/key to use
555
- index: Index for widgets with duplicate names (default: 0)
556
- defer: If True, queues the update for later batch processing
503
+ old_key (str): The old key of the widget that you want to rename.
504
+ new_key (str): The new key to assign to the widget.
505
+ index (int): The index of the widget if there are multiple widgets with the same name (default: 0).
506
+ defer (bool): Whether to defer the update. If True, the update is added to a queue and applied
507
+ when `commit_widget_key_updates` is called. If False, the update is applied immediately (default: False).
557
508
 
558
509
  Returns:
559
- PdfWrapper: Returns self to allow method chaining
560
-
561
- Raises:
562
- NotImplementedError: When use_full_widget_name is enabled
510
+ PdfWrapper: The PdfWrapper object.
563
511
  """
564
512
 
565
513
  if getattr(self, "use_full_widget_name"):
@@ -569,7 +517,8 @@ class PdfWrapper(FormWrapper):
569
517
  self._keys_to_update.append((old_key, new_key, index))
570
518
  return self
571
519
 
572
- self.stream = update_widget_keys(
520
+ self._key_update_tracker[new_key] = old_key
521
+ self._stream = update_widget_keys(
573
522
  self.read(), self.widgets, [old_key], [new_key], [index]
574
523
  )
575
524
  self._init_helper()
@@ -577,17 +526,14 @@ class PdfWrapper(FormWrapper):
577
526
  return self
578
527
 
579
528
  def commit_widget_key_updates(self) -> PdfWrapper:
580
- """Processes all deferred widget key updates in a single batch operation.
529
+ """
530
+ Commits deferred widget key updates, applying all queued key renames to the PDF.
581
531
 
582
- Applies all key updates that were queued using update_widget_key() with
583
- defer=True. This is more efficient than individual updates when renaming
584
- multiple fields.
532
+ This method applies all widget key updates that were deferred using the `defer=True` option
533
+ in the `update_widget_key` method. It updates the underlying PDF stream with the new key names.
585
534
 
586
535
  Returns:
587
- PdfWrapper: Returns self to allow method chaining
588
-
589
- Raises:
590
- NotImplementedError: When use_full_widget_name is enabled
536
+ PdfWrapper: The PdfWrapper object.
591
537
  """
592
538
 
593
539
  if getattr(self, "use_full_widget_name"):
@@ -597,9 +543,12 @@ class PdfWrapper(FormWrapper):
597
543
  new_keys = [each[1] for each in self._keys_to_update]
598
544
  indices = [each[2] for each in self._keys_to_update]
599
545
 
600
- self.stream = update_widget_keys(
546
+ self._stream = update_widget_keys(
601
547
  self.read(), self.widgets, old_keys, new_keys, indices
602
548
  )
549
+
550
+ for each in self._keys_to_update:
551
+ self._key_update_tracker[each[1]] = each[0]
603
552
  self._init_helper()
604
553
  self._keys_to_update = []
605
554
 
@@ -613,26 +562,21 @@ class PdfWrapper(FormWrapper):
613
562
  y: Union[float, int],
614
563
  **kwargs,
615
564
  ) -> PdfWrapper:
616
- """Draws static text onto the PDF document at specified coordinates.
617
-
618
- Adds non-interactive text that becomes part of the PDF content rather
619
- than a form field. The text is drawn using a temporary Text widget and
620
- merged via watermark operations, preserving existing form fields.
621
-
622
- Supports multi-line text (using NEW_LINE_SYMBOL) and custom formatting.
565
+ """
566
+ Draws text on the PDF.
623
567
 
624
568
  Args:
625
- text: The text content to draw (supports newlines with NEW_LINE_SYMBOL)
626
- page_number: Page number (1-based) to draw text on
627
- x: X coordinate for text position
628
- y: Y coordinate for text position
629
- **kwargs: Text formatting options:
630
- font: Font name (default: "Helvetica")
631
- font_size: Font size in points (default: 12)
632
- font_color: Font color as RGB tuple (default: (0, 0, 0))
569
+ text (str): The text to draw.
570
+ page_number (int): The page number to draw on.
571
+ x (Union[float, int]): The x coordinate of the text.
572
+ y (Union[float, int]): The y coordinate of the text.
573
+ **kwargs: Additional keyword arguments:
574
+ - `font` (str): The name of the font to use (default: DEFAULT_FONT).
575
+ - `font_size` (float): The font size in points (default: DEFAULT_FONT_SIZE).
576
+ - `font_color` (Tuple[float, float, float]): The font color as an RGB tuple (default: DEFAULT_FONT_COLOR).
633
577
 
634
578
  Returns:
635
- PdfWrapper: Returns self to allow method chaining
579
+ PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
636
580
  """
637
581
 
638
582
  new_widget = Text("new")
@@ -641,11 +585,8 @@ class PdfWrapper(FormWrapper):
641
585
  new_widget.font_size = kwargs.get("font_size", DEFAULT_FONT_SIZE)
642
586
  new_widget.font_color = kwargs.get("font_color", DEFAULT_FONT_COLOR)
643
587
 
644
- if NEW_LINE_SYMBOL in text:
645
- new_widget.text_lines = text.split(NEW_LINE_SYMBOL)
646
-
647
588
  watermarks = create_watermarks_and_draw(
648
- self.stream,
589
+ self.read(),
649
590
  page_number,
650
591
  "text",
651
592
  [
@@ -658,10 +599,12 @@ class PdfWrapper(FormWrapper):
658
599
  )
659
600
 
660
601
  stream_with_widgets = self.read()
661
- self.stream = merge_watermarks_with_pdf(self.stream, watermarks)
662
- self.stream = copy_watermark_widgets(
663
- remove_all_widgets(self.stream), stream_with_widgets, None, None
602
+ self._stream = merge_watermarks_with_pdf(self.read(), watermarks)
603
+ self._stream = copy_watermark_widgets(
604
+ remove_all_widgets(self.read()), stream_with_widgets, None, None
664
605
  )
606
+ # because copy_watermark_widgets and remove_all_widgets
607
+ self._reregister_font()
665
608
 
666
609
  return self
667
610
 
@@ -675,82 +618,75 @@ class PdfWrapper(FormWrapper):
675
618
  height: Union[float, int],
676
619
  rotation: Union[float, int] = 0,
677
620
  ) -> PdfWrapper:
678
- """Draws an image onto the PDF document at specified coordinates.
679
-
680
- The image is merged via watermark operations, preserving existing form fields.
681
- Supports common formats (JPEG, PNG) from bytes, file paths, or file objects.
621
+ """
622
+ Draws an image on the PDF.
682
623
 
683
624
  Args:
684
- image: Image data as bytes, file path, or file object
685
- page_number: Page number (1-based) to draw image on
686
- x: X coordinate for image position (lower-left corner)
687
- y: Y coordinate for image position (lower-left corner)
688
- width: Width of the drawn image in PDF units
689
- height: Height of the drawn image in PDF units
690
- rotation: Rotation angle in degrees (default: 0)
625
+ image (Union[bytes, str, BinaryIO]): The image data, provided as either:
626
+ - bytes: The raw image data as a byte string.
627
+ - str: The file path to the image.
628
+ - BinaryIO: An open file-like object containing the image data.
629
+ page_number (int): The page number to draw the image on.
630
+ x (Union[float, int]): The x coordinate of the image.
631
+ y (Union[float, int]): The y coordinate of the image.
632
+ width (Union[float, int]): The width of the image.
633
+ height (Union[float, int]): The height of the image.
634
+ rotation (Union[float, int]): The rotation of the image in degrees (default: 0).
691
635
 
692
636
  Returns:
693
- PdfWrapper: Returns self to allow method chaining
637
+ PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
694
638
  """
695
639
 
696
640
  image = fp_or_f_obj_or_stream_to_stream(image)
697
641
  image = rotate_image(image, rotation)
698
642
  watermarks = create_watermarks_and_draw(
699
- self.stream,
643
+ self.read(),
700
644
  page_number,
701
645
  "image",
702
646
  [{"stream": image, "x": x, "y": y, "width": width, "height": height}],
703
647
  )
704
648
 
705
649
  stream_with_widgets = self.read()
706
- self.stream = merge_watermarks_with_pdf(self.stream, watermarks)
707
- self.stream = copy_watermark_widgets(
708
- remove_all_widgets(self.stream), stream_with_widgets, None, None
650
+ self._stream = merge_watermarks_with_pdf(self.read(), watermarks)
651
+ self._stream = copy_watermark_widgets(
652
+ remove_all_widgets(self.read()), stream_with_widgets, None, None
709
653
  )
654
+ # because copy_watermark_widgets and remove_all_widgets
655
+ self._reregister_font()
710
656
 
711
657
  return self
712
658
 
713
- @property
714
- def schema(self) -> dict:
715
- """Generates a JSON schema describing the PDF form's fields and types.
716
-
717
- The schema includes:
718
- - Field names as property names
719
- - Type information (string, boolean, integer)
720
- - Field-specific constraints like max lengths for text fields
721
- - Choice indices for dropdown fields
722
-
723
- Note: Does not include required field indicators since the PDF form's
724
- validation rules are not extracted.
725
-
726
- Returns:
727
- dict: A JSON Schema dictionary following Draft 7 format
728
- """
729
-
730
- return {
731
- "type": "object",
732
- "properties": {
733
- key: value.schema_definition for key, value in self.widgets.items()
734
- },
735
- }
736
-
737
- @classmethod
738
659
  def register_font(
739
- cls, font_name: str, ttf_file: Union[bytes, str, BinaryIO]
740
- ) -> bool:
741
- """Class method to register a TrueType font for use in PDF form text fields.
742
-
743
- Registers the font globally so it can be used by all PdfWrapper instances.
744
- The font will be available when specified by name in text operations.
660
+ self,
661
+ font_name: str,
662
+ ttf_file: Union[bytes, str, BinaryIO],
663
+ first_time: bool = True,
664
+ ) -> PdfWrapper:
665
+ """
666
+ Registers a custom font for use in the PDF.
745
667
 
746
668
  Args:
747
- font_name: Name to register the font under (used when setting font)
748
- ttf_file: The TTF font data as bytes, file path, or file object
669
+ font_name (str): The name of the font. This name will be used to reference the font when drawing text.
670
+ ttf_file (Union[bytes, str, BinaryIO]): The TTF file data, provided as either:
671
+ - bytes: The raw TTF file data as a byte string.
672
+ - str: The file path to the TTF file.
673
+ - BinaryIO: An open file-like object containing the TTF file data.
674
+ first_time (bool): Whether this is the first time the font is being registered (default: True).
675
+ If True and `adobe_mode` is enabled, a blank text string is drawn to ensure the font is properly embedded in the PDF.
749
676
 
750
677
  Returns:
751
- bool: True if registration succeeded, False if failed
678
+ PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
752
679
  """
753
680
 
754
681
  ttf_file = fp_or_f_obj_or_stream_to_stream(ttf_file)
755
682
 
756
- return register_font(font_name, ttf_file) if ttf_file is not None else False
683
+ if register_font(font_name, ttf_file) if ttf_file is not None else False:
684
+ if first_time and getattr(self, "adobe_mode"):
685
+ self.draw_text(" ", 1, 0, 0, font=font_name)
686
+ self._stream, new_font_name = register_font_acroform(
687
+ self.read(), ttf_file, getattr(self, "adobe_mode")
688
+ )
689
+ self._available_fonts[font_name] = new_font_name
690
+ self._font_register_events.append((font_name, ttf_file))
691
+
692
+ return self