PyPDFForm 2.5.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/filler.py CHANGED
@@ -1,106 +1,60 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Provides core functionality for filling PDF form fields.
3
-
4
- This module handles:
5
- - Drawing text, images, borders and other elements onto PDF forms
6
- - Managing widget states and appearances
7
- - Supporting different filling modes (simple vs watermark-based)
8
- - Handling special cases like checkboxes, radio buttons and signatures
2
+ """
3
+ Module containing functions to fill PDF forms.
9
4
 
10
- The main functions are:
11
- - fill(): Uses watermark technique for complex form filling
12
- - simple_fill(): Directly modifies form fields for simpler cases
5
+ This module provides the core functionality for filling PDF forms programmatically.
6
+ It includes functions for handling various form field types, such as text fields,
7
+ checkboxes, radio buttons, dropdowns, images, and signatures. The module also
8
+ supports flattening the filled form to prevent further modifications.
13
9
  """
14
10
 
15
11
  from io import BytesIO
16
- from typing import Dict, Tuple, Union, cast
12
+ from typing import Dict, Union, cast
17
13
 
18
14
  from pypdf import PdfReader, PdfWriter
19
- from pypdf.generic import (ArrayObject, BooleanObject, DictionaryObject,
20
- IndirectObject, NameObject)
15
+ from pypdf.generic import DictionaryObject
21
16
 
22
- from .constants import (BUTTON_STYLES, DEFAULT_RADIO_STYLE, WIDGET_TYPES,
23
- AcroForm, Annots, Fields, NeedAppearances, Root, U)
24
- from .coordinate import (get_draw_border_coordinates,
25
- get_draw_checkbox_radio_coordinates,
26
- get_draw_image_coordinates_resolutions,
27
- get_draw_text_coordinates,
28
- get_text_line_x_coordinates)
29
- from .font import checkbox_radio_font_size
30
- from .image import get_image_dimensions
17
+ from .constants import WIDGET_TYPES, Annots
18
+ from .image import get_draw_image_resolutions, get_image_dimensions
31
19
  from .middleware.checkbox import Checkbox
32
20
  from .middleware.dropdown import Dropdown
33
21
  from .middleware.image import Image
34
22
  from .middleware.radio import Radio
35
23
  from .middleware.signature import Signature
36
24
  from .middleware.text import Text
37
- from .patterns import (simple_flatten_generic, simple_flatten_radio,
38
- simple_update_checkbox_value,
39
- simple_update_dropdown_value, simple_update_radio_value,
40
- simple_update_text_value)
41
- from .template import get_widget_key, get_widgets_by_page
42
- from .utils import checkbox_radio_to_draw, stream_to_io
25
+ from .patterns import (flatten_generic, flatten_radio, update_checkbox_value,
26
+ update_dropdown_value, update_radio_value,
27
+ update_text_value)
28
+ from .template import get_widget_key
29
+ from .utils import stream_to_io
43
30
  from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
44
31
 
45
32
 
46
- def check_radio_handler(
47
- widget: dict, middleware: Union[Checkbox, Radio], radio_button_tracker: dict
48
- ) -> Tuple[Text, Union[float, int], Union[float, int], bool]:
49
- """Calculates drawing parameters for checkbox and radio button widgets.
50
-
51
- Args:
52
- widget: PDF form widget dictionary containing Rect coordinates
53
- middleware: Checkbox or Radio middleware instance
54
- radio_button_tracker: Dictionary tracking radio button group states
55
-
56
- Returns:
57
- Tuple containing:
58
- - Text: Prepared text object for drawing the symbol
59
- - float/int: x coordinate for drawing
60
- - float/int: y coordinate for drawing
61
- - bool: Whether the symbol needs to be drawn
62
- """
63
-
64
- font_size = (
65
- checkbox_radio_font_size(widget) if middleware.size is None else middleware.size
66
- )
67
- to_draw = checkbox_radio_to_draw(middleware, font_size)
68
- x, y = get_draw_checkbox_radio_coordinates(
69
- widget, to_draw, border_width=middleware.border_width
70
- )
71
- text_needs_to_be_drawn = False
72
- if type(middleware) is Checkbox and middleware.value:
73
- text_needs_to_be_drawn = True
74
- elif isinstance(middleware, Radio):
75
- if middleware.name not in radio_button_tracker:
76
- radio_button_tracker[middleware.name] = 0
77
- radio_button_tracker[middleware.name] += 1
78
- if middleware.value == radio_button_tracker[middleware.name] - 1:
79
- text_needs_to_be_drawn = True
80
-
81
- return to_draw, x, y, text_needs_to_be_drawn
82
-
83
-
84
33
  def signature_image_handler(
85
34
  widget: dict, middleware: Union[Signature, Image], images_to_draw: list
86
35
  ) -> bool:
87
- """Prepares image data for signature and image widgets.
36
+ """Handles signature and image widgets by extracting image data and preparing it for drawing.
37
+
38
+ This function processes signature and image widgets found in a PDF form. It extracts the
39
+ image data from the widget's middleware and prepares it for drawing on the form. The
40
+ function calculates the position and dimensions of the image based on the widget's
41
+ properties and the `preserve_aspect_ratio` setting. The image data is then stored in a
42
+ list for later drawing.
88
43
 
89
44
  Args:
90
- widget: PDF form widget dictionary containing Rect coordinates
91
- middleware: Signature or Image middleware instance
92
- images_to_draw: List to append image drawing parameters to
45
+ widget (dict): The widget dictionary representing the signature or image field.
46
+ middleware (Union[Signature, Image]): The middleware object containing the image data and properties.
47
+ images_to_draw (list): A list to store image data for drawing.
93
48
 
94
49
  Returns:
95
- bool: True if an image needs to be drawn, False otherwise
50
+ bool: True if any image is to be drawn, False otherwise.
96
51
  """
97
-
98
52
  stream = middleware.stream
99
53
  any_image_to_draw = False
100
54
  if stream is not None:
101
55
  any_image_to_draw = True
102
56
  image_width, image_height = get_image_dimensions(stream)
103
- x, y, width, height = get_draw_image_coordinates_resolutions(
57
+ x, y, width, height = get_draw_image_resolutions(
104
58
  widget, middleware.preserve_aspect_ratio, image_width, image_height
105
59
  )
106
60
  images_to_draw.append(
@@ -116,95 +70,23 @@ def signature_image_handler(
116
70
  return any_image_to_draw
117
71
 
118
72
 
119
- def text_handler(
120
- widget: dict, middleware: Text
121
- ) -> Tuple[Text, Union[float, int], Union[float, int], bool]:
122
- """Prepares text field drawing parameters.
123
-
124
- Args:
125
- widget: PDF form widget dictionary containing Rect and properties
126
- middleware: Text middleware instance with text properties
127
-
128
- Returns:
129
- Tuple containing:
130
- - Text: The text middleware to draw
131
- - float/int: x coordinate for drawing
132
- - float/int: y coordinate for drawing
133
- - bool: Always True for text fields (they always need drawing)
134
- """
135
-
136
- middleware.text_line_x_coordinates = get_text_line_x_coordinates(widget, middleware)
137
- x, y = get_draw_text_coordinates(widget, middleware)
138
- to_draw = middleware
139
- text_needs_to_be_drawn = True
140
-
141
- return to_draw, x, y, text_needs_to_be_drawn
142
-
143
-
144
- def border_handler(
145
- widget: dict,
146
- middleware: WIDGET_TYPES,
147
- rect_borders_to_draw: list,
148
- ellipse_borders_to_draw: list,
149
- line_borders_to_draw: list,
150
- ) -> None:
151
- """Prepares border drawing parameters for widgets.
152
-
153
- Args:
154
- widget: PDF form widget dictionary containing Rect coordinates
155
- middleware: Any widget middleware instance
156
- rect_borders_to_draw: List to append rectangle border parameters to
157
- ellipse_borders_to_draw: List to append ellipse border parameters to
158
- line_borders_to_draw: List to append line border parameters to
159
- """
160
-
161
- if (
162
- isinstance(middleware, Radio)
163
- and BUTTON_STYLES.get(middleware.button_style) == DEFAULT_RADIO_STYLE
164
- ):
165
- list_to_append = ellipse_borders_to_draw
166
- shape = "ellipse"
167
- elif middleware.border_style == U:
168
- list_to_append = line_borders_to_draw
169
- shape = "line"
170
- else:
171
- list_to_append = rect_borders_to_draw
172
- shape = "rect"
173
-
174
- list_to_append.append(
175
- {
176
- **get_draw_border_coordinates(widget, shape),
177
- "border_color": middleware.border_color,
178
- "background_color": middleware.background_color,
179
- "border_width": middleware.border_width,
180
- "dash_array": middleware.dash_array,
181
- }
182
- )
183
-
184
- if shape == "line":
185
- rect_borders_to_draw.append(
186
- {
187
- **get_draw_border_coordinates(widget, "rect"),
188
- "border_color": None,
189
- "background_color": middleware.background_color,
190
- "border_width": 0,
191
- "dash_array": None,
192
- }
193
- )
194
-
195
-
196
73
  def get_drawn_stream(to_draw: dict, stream: bytes, action: str) -> bytes:
197
- """Applies drawing operations to a PDF stream.
74
+ """Applies watermarks to specific pages of a PDF based on the provided drawing instructions.
75
+
76
+ This function takes a dictionary of drawing instructions and applies watermarks to the
77
+ specified pages of a PDF. It iterates through the drawing instructions, creates watermarks
78
+ for each page, and merges the watermarks with the original PDF content. The function
79
+ supports various drawing actions, such as adding images or text.
198
80
 
199
81
  Args:
200
- to_draw: Dictionary mapping page numbers to drawing parameters
201
- stream: Input PDF as bytes
202
- action: Type of drawing operation ('text', 'image', 'rect', etc.)
82
+ to_draw (dict): A dictionary containing page numbers as keys and lists of drawing instructions as values.
83
+ Each drawing instruction specifies the type of drawing, position, dimensions, and content.
84
+ stream (bytes): The PDF content as bytes.
85
+ action (str): The type of action to perform (e.g., "image", "text").
203
86
 
204
87
  Returns:
205
- bytes: Modified PDF with drawings applied
88
+ bytes: The modified PDF content with watermarks applied.
206
89
  """
207
-
208
90
  watermark_list = []
209
91
  for page, stuffs in to_draw.items():
210
92
  watermark_list.append(b"")
@@ -217,184 +99,78 @@ def get_drawn_stream(to_draw: dict, stream: bytes, action: str) -> bytes:
217
99
 
218
100
 
219
101
  def fill(
220
- template_stream: bytes,
221
- widgets: Dict[str, WIDGET_TYPES],
222
- use_full_widget_name: bool,
223
- ) -> bytes:
224
- """Fills a PDF form using watermark technique for complex rendering.
225
-
226
- This method:
227
- - Handles text, images, borders for all widget types
228
- - Preserves original form fields while adding visual elements
229
- - Supports complex cases like multiline text and image scaling
230
-
231
- Args:
232
- template_stream: Input PDF form as bytes
233
- widgets: Dictionary mapping field names to widget middleware
234
- use_full_widget_name: If True, uses the full widget name as the key in the widgets dictionary
235
-
236
- Returns:
237
- bytes: Filled PDF form as bytes
238
- """
239
-
240
- texts_to_draw = {}
241
- images_to_draw = {}
242
- rect_borders_to_draw = {}
243
- ellipse_borders_to_draw = {}
244
- line_borders_to_draw = {}
245
- any_image_to_draw = False
246
-
247
- radio_button_tracker = {}
248
-
249
- for page, widget_dicts in get_widgets_by_page(template_stream).items():
250
- texts_to_draw[page] = []
251
- images_to_draw[page] = []
252
- rect_borders_to_draw[page] = []
253
- ellipse_borders_to_draw[page] = []
254
- line_borders_to_draw[page] = []
255
- for widget_dict in widget_dicts:
256
- key = get_widget_key(widget_dict, use_full_widget_name)
257
- text_needs_to_be_drawn = False
258
- to_draw = x = y = None
259
-
260
- if widgets[key].render_widget:
261
- border_handler(
262
- widget_dict,
263
- widgets[key],
264
- rect_borders_to_draw[page],
265
- ellipse_borders_to_draw[page],
266
- line_borders_to_draw[page],
267
- )
268
-
269
- if isinstance(widgets[key], (Checkbox, Radio)):
270
- to_draw, x, y, text_needs_to_be_drawn = check_radio_handler(
271
- widget_dict, widgets[key], radio_button_tracker
272
- )
273
- elif isinstance(widgets[key], (Signature, Image)):
274
- any_image_to_draw |= signature_image_handler(
275
- widget_dict, widgets[key], images_to_draw[page]
276
- )
277
- else:
278
- to_draw, x, y, text_needs_to_be_drawn = text_handler(
279
- widget_dict, widgets[key]
280
- )
281
-
282
- if all(
283
- [
284
- text_needs_to_be_drawn,
285
- to_draw is not None,
286
- x is not None,
287
- y is not None,
288
- ]
289
- ):
290
- texts_to_draw[page].append(
291
- {
292
- "widget": to_draw,
293
- "x": x,
294
- "y": y,
295
- }
296
- )
297
-
298
- result = template_stream
299
- result = get_drawn_stream(rect_borders_to_draw, result, "rect")
300
- result = get_drawn_stream(ellipse_borders_to_draw, result, "ellipse")
301
- result = get_drawn_stream(line_borders_to_draw, result, "line")
302
- result = get_drawn_stream(texts_to_draw, result, "text")
303
-
304
- if any_image_to_draw:
305
- result = get_drawn_stream(images_to_draw, result, "image")
306
-
307
- return result
308
-
309
-
310
- def enable_adobe_mode(reader: PdfReader, writer: PdfWriter, adobe_mode: bool) -> None:
311
- """Configures the PDF for Adobe Acrobat compatibility by setting the NeedAppearances flag
312
- and ensuring the AcroForm structure is properly initialized.
313
-
314
- Args:
315
- reader: PdfReader instance of the PDF
316
- writer: PdfWriter instance to configure
317
- adobe_mode: If True, enables Adobe Acrobat compatibility mode
318
- """
319
-
320
- if not adobe_mode:
321
- return
322
-
323
- # https://stackoverflow.com/questions/47288578/pdf-form-filled-with-pypdf2-does-not-show-in-print
324
- if AcroForm in reader.trailer[Root]:
325
- reader.trailer[Root][AcroForm].update(
326
- {NameObject(NeedAppearances): BooleanObject(True)}
327
- )
328
-
329
- if AcroForm not in writer.root_object:
330
- writer.root_object.update(
331
- {NameObject(AcroForm): IndirectObject(len(writer.root_object), 0, writer)}
332
- )
333
- writer.root_object[AcroForm][NameObject(NeedAppearances)] = BooleanObject(True)
334
- writer.root_object[AcroForm][NameObject(Fields)] = ArrayObject()
335
-
336
-
337
- def simple_fill(
338
102
  template: bytes,
339
103
  widgets: Dict[str, WIDGET_TYPES],
340
104
  use_full_widget_name: bool,
341
105
  flatten: bool = False,
342
- adobe_mode: bool = False,
343
- ) -> bytes:
344
- """Fills a PDF form by directly modifying form fields.
106
+ ) -> tuple:
107
+ """Fills a PDF template with the given widgets.
345
108
 
346
- This method:
347
- - Updates field values directly in the PDF
348
- - Supports flattening to make fields read-only
349
- - Works with Adobe Acrobat compatibility mode
109
+ This function fills a PDF template with the provided widget values. It iterates through the
110
+ widgets on each page of the PDF and updates their values based on the provided `widgets`
111
+ dictionary. The function supports various widget types, including text fields, checkboxes,
112
+ radio buttons, dropdowns, images, and signatures. It also supports flattening the filled
113
+ form to prevent further modifications.
350
114
 
351
115
  Args:
352
- template: Input PDF form as bytes
353
- widgets: Dictionary mapping field names to widget middleware
354
- use_full_widget_name: If True, uses the full widget name as the key in the widgets dictionary
355
- flatten: If True, makes form fields read-only
356
- adobe_mode: If True, enables Adobe Acrobat compatibility
116
+ template (bytes): The PDF template as bytes.
117
+ widgets (Dict[str, WIDGET_TYPES]): A dictionary of widgets to fill, where the keys are the
118
+ widget names and the values are the widget objects.
119
+ use_full_widget_name (bool): Whether to use the full widget name when looking up widgets
120
+ in the `widgets` dictionary.
121
+ flatten (bool): Whether to flatten the filled PDF. Defaults to False.
357
122
 
358
123
  Returns:
359
- bytes: Filled PDF form as bytes
124
+ tuple: A tuple containing the filled PDF as bytes and the image drawn stream as bytes, if any.
125
+ The image drawn stream is only returned if there are any image or signature widgets
126
+ in the form.
360
127
  """
361
-
362
128
  pdf = PdfReader(stream_to_io(template))
363
129
  out = PdfWriter()
364
- enable_adobe_mode(pdf, out, adobe_mode)
365
130
  out.append(pdf)
366
131
 
367
132
  radio_button_tracker = {}
133
+ images_to_draw = {}
134
+ any_image_to_draw = False
368
135
 
369
- for page in out.pages:
136
+ for page_num, page in enumerate(out.pages):
137
+ images_to_draw[page_num + 1] = []
370
138
  for annot in page.get(Annots, []):
371
139
  annot = cast(DictionaryObject, annot.get_object())
372
140
  key = get_widget_key(annot.get_object(), use_full_widget_name)
373
141
 
374
142
  widget = widgets.get(key)
375
- if widget is None or widget.value is None:
143
+ if widget is None:
144
+ continue
145
+
146
+ # flatten all
147
+ if flatten:
148
+ (flatten_radio if isinstance(widget, Radio) else flatten_generic)(annot)
149
+ if widget.value is None:
376
150
  continue
377
151
 
378
- if type(widget) is Checkbox:
379
- simple_update_checkbox_value(annot, widget.value)
152
+ if isinstance(widgets[key], (Signature, Image)):
153
+ any_image_to_draw |= signature_image_handler(
154
+ annot, widgets[key], images_to_draw[page_num + 1]
155
+ )
156
+ elif type(widget) is Checkbox:
157
+ update_checkbox_value(annot, widget.value)
380
158
  elif isinstance(widget, Radio):
381
159
  if key not in radio_button_tracker:
382
160
  radio_button_tracker[key] = 0
383
161
  radio_button_tracker[key] += 1
384
162
  if widget.value == radio_button_tracker[key] - 1:
385
- simple_update_radio_value(annot)
163
+ update_radio_value(annot)
386
164
  elif isinstance(widget, Dropdown):
387
- simple_update_dropdown_value(annot, widget)
165
+ update_dropdown_value(annot, widget)
388
166
  elif isinstance(widget, Text):
389
- simple_update_text_value(annot, widget)
390
-
391
- if flatten:
392
- if isinstance(widget, Radio):
393
- simple_flatten_radio(annot)
394
- else:
395
- simple_flatten_generic(annot)
167
+ update_text_value(annot, widget)
396
168
 
397
169
  with BytesIO() as f:
398
170
  out.write(f)
399
171
  f.seek(0)
400
- return f.read()
172
+ result = f.read()
173
+
174
+ return result, (
175
+ get_drawn_stream(images_to_draw, result, "image") if any_image_to_draw else None
176
+ )