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