PyPDFForm 2.0.0__py3-none-any.whl → 2.1.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/__init__.py CHANGED
@@ -1,7 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Contains any object users might need."""
2
+ """PyPDFForm package for PDF form filling and manipulation.
3
3
 
4
- __version__ = "2.0.0"
4
+ This package provides tools for filling PDF forms, drawing text and images,
5
+ and manipulating PDF form elements programmatically.
6
+ """
7
+
8
+ __version__ = "2.1.0"
5
9
 
6
10
  from .wrapper import FormWrapper, PdfWrapper
7
11
 
PyPDFForm/adapter.py CHANGED
@@ -1,12 +1,31 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Contains user input adapters."""
2
+ """Provides adapters for handling different types of file inputs.
3
+
4
+ This module contains utility functions for working with various file input types:
5
+ - Raw bytes
6
+ - File paths
7
+ - File-like objects
8
+
9
+ The adapters normalize these different input types into consistent byte streams
10
+ that can be processed by the PDF manipulation functions.
11
+ """
3
12
 
4
13
  from os.path import isfile
5
14
  from typing import Any, BinaryIO, Union
6
15
 
7
16
 
8
17
  def readable(obj: Any) -> bool:
9
- """Checks if an object is readable."""
18
+ """Determines if an object is file-like and readable.
19
+
20
+ Checks if the object has a callable read() method, indicating it can be
21
+ treated as a file-like object for reading operations.
22
+
23
+ Args:
24
+ obj: The object to check for read capability
25
+
26
+ Returns:
27
+ bool: True if the object has a callable read() method, False otherwise
28
+ """
10
29
 
11
30
  return callable(getattr(obj, "read", None))
12
31
 
@@ -14,7 +33,22 @@ def readable(obj: Any) -> bool:
14
33
  def fp_or_f_obj_or_stream_to_stream(
15
34
  fp_or_f_obj_or_stream: Union[bytes, str, BinaryIO],
16
35
  ) -> bytes:
17
- """Converts a file path or a file object to a stream."""
36
+ """Converts various file input types to a byte stream.
37
+
38
+ Handles conversion of:
39
+ - Raw bytes (passed through unchanged)
40
+ - File paths (reads file contents)
41
+ - File-like objects (reads using read() method)
42
+
43
+ Args:
44
+ fp_or_f_obj_or_stream: Input to convert, which can be:
45
+ - bytes: Raw PDF data
46
+ - str: Path to PDF file
47
+ - BinaryIO: File-like object containing PDF data
48
+
49
+ Returns:
50
+ bytes: The PDF data as a byte stream
51
+ """
18
52
 
19
53
  result = b""
20
54
  if isinstance(fp_or_f_obj_or_stream, bytes):
PyPDFForm/constants.py CHANGED
@@ -1,5 +1,16 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Contains library constants."""
2
+ """Centralized location for all PyPDFForm constants and default values.
3
+
4
+ This module defines all the constants used throughout the package including:
5
+ - PDF version identifiers and format strings
6
+ - PDF field type and property constants
7
+ - Default values for fonts, colors and sizes
8
+ - Special identifiers and symbols
9
+ - Field flag bitmasks
10
+ - Button style definitions
11
+
12
+ All constants are organized by category and used consistently across the package.
13
+ """
3
14
 
4
15
  from typing import Union
5
16
 
PyPDFForm/coordinate.py CHANGED
@@ -1,5 +1,17 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Contains helpers for coordinates calculations."""
2
+ """Provides coordinate calculation utilities for PDF form elements.
3
+
4
+ This module contains functions for calculating positions and dimensions
5
+ for drawing various PDF form elements including:
6
+ - Text fields and paragraphs
7
+ - Checkboxes and radio buttons
8
+ - Images and signatures
9
+ - Borders and decorative elements
10
+
11
+ All calculations work in PDF coordinate space where:
12
+ - Origin (0,0) is at bottom-left corner
13
+ - Units are in PDF points (1/72 inch)
14
+ """
3
15
 
4
16
  from copy import deepcopy
5
17
  from typing import List, Tuple, Union
@@ -17,7 +29,17 @@ from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
17
29
 
18
30
 
19
31
  def get_draw_border_coordinates(widget: dict, shape: str) -> List[float]:
20
- """Returns coordinates to draw each widget's border."""
32
+ """Calculates coordinates for drawing widget borders.
33
+
34
+ Args:
35
+ widget: PDF form widget dictionary containing Rect coordinates
36
+ shape: Type of border to draw ("rectangle", "ellipse" or "line")
37
+
38
+ Returns:
39
+ List[float]: Coordinates in format [x1, y1, x2, y2] for the border
40
+ For ellipses: [center_x, center_y, radius_x, radius_y]
41
+ For lines: [x1, y1, x2, y2] endpoints
42
+ """
21
43
 
22
44
  result = [
23
45
  float(widget[Rect][0]),
@@ -57,7 +79,17 @@ def get_draw_checkbox_radio_coordinates(
57
79
  widget_middleware: Text,
58
80
  border_width: int,
59
81
  ) -> Tuple[Union[float, int], Union[float, int]]:
60
- """Returns coordinates to draw at given a PDF form checkbox/radio widget."""
82
+ """Calculates drawing coordinates for checkbox/radio button symbols.
83
+
84
+ Args:
85
+ widget: PDF form widget dictionary containing Rect coordinates
86
+ widget_middleware: Text middleware containing font properties
87
+ border_width: Width of widget border in points
88
+
89
+ Returns:
90
+ Tuple[Union[float, int], Union[float, int]]: (x, y) coordinates
91
+ for drawing the checkbox/radio symbol
92
+ """
61
93
 
62
94
  string_height = widget_middleware.font_size * 72 / 96
63
95
  width_mid_point = (float(widget[Rect][0]) + float(widget[Rect][2])) / 2
@@ -83,8 +115,18 @@ def get_draw_image_coordinates_resolutions(
83
115
  image_width: float,
84
116
  image_height: float,
85
117
  ) -> Tuple[float, float, float, float]:
86
- """
87
- Returns coordinates and resolutions to draw image at given a PDF form signature/image widget.
118
+ """Calculates image drawing coordinates and scaling factors.
119
+
120
+ Args:
121
+ widget: PDF form widget dictionary containing Rect coordinates
122
+ preserve_aspect_ratio: Whether to maintain image proportions
123
+ image_width: Original width of the image in points
124
+ image_height: Original height of the image in points
125
+
126
+ Returns:
127
+ Tuple[float, float, float, float]: (x, y, width, height) where:
128
+ x,y: Bottom-left corner coordinates
129
+ width,height: Scaled dimensions for drawing
88
130
  """
89
131
 
90
132
  x = float(widget[Rect][0])
@@ -107,17 +149,161 @@ def get_draw_image_coordinates_resolutions(
107
149
  return x, y, width, height
108
150
 
109
151
 
152
+ def calculate_text_coord_x(
153
+ widget: dict,
154
+ widget_middleware: Text,
155
+ text_value: str,
156
+ length: int,
157
+ character_paddings: List[float],
158
+ ) -> float:
159
+ """
160
+ Calculate the horizontal (x) coordinate for text drawing within a PDF form field.
161
+
162
+ This function determines the x-coordinate based on:
163
+ - The widget's alignment setting (left, center, right)
164
+ - Whether the field uses comb formatting
165
+ - The width of the text string
166
+ - Character paddings for comb fields
167
+
168
+ Args:
169
+ widget: PDF form widget dictionary containing Rect and alignment info.
170
+ widget_middleware: Text middleware containing font and comb properties.
171
+ text_value: The text string to be drawn (already trimmed).
172
+ length: The length of the text string.
173
+ character_paddings: List of cumulative paddings for comb characters.
174
+
175
+ Returns:
176
+ float: The calculated x-coordinate for the text baseline start.
177
+ """
178
+
179
+ alignment = (
180
+ extract_widget_property(widget, WIDGET_ALIGNMENT_PATTERNS, None, int) or 0
181
+ )
182
+ # Default to left boundary
183
+ x = float(widget[Rect][0])
184
+
185
+ if int(alignment) == 0:
186
+ return x
187
+
188
+ # Calculate the horizontal midpoint of the widget rectangle
189
+ width_mid_point = (float(widget[Rect][0]) + float(widget[Rect][2])) / 2
190
+
191
+ # Calculate the width of the entire string in points
192
+ string_width = stringWidth(
193
+ text_value,
194
+ widget_middleware.font,
195
+ widget_middleware.font_size,
196
+ )
197
+
198
+ # If comb formatting, adjust string width to include last character's right padding
199
+ if widget_middleware.comb is True and length:
200
+ string_width = character_paddings[-1] + stringWidth(
201
+ text_value[-1],
202
+ widget_middleware.font,
203
+ widget_middleware.font_size,
204
+ )
205
+
206
+ if int(alignment) == 1: # Center alignment
207
+ # Center text by offsetting half the string width from the midpoint
208
+ x = width_mid_point - string_width / 2
209
+ elif int(alignment) == 2: # Right alignment
210
+ # Align text to the right edge minus the string width
211
+ x = float(widget[Rect][2]) - string_width
212
+ if length > 0 and widget_middleware.comb is True:
213
+ # For comb fields, adjust further by half the difference between comb box width and last character width
214
+ x -= (
215
+ get_char_rect_width(widget, widget_middleware)
216
+ - stringWidth(
217
+ text_value[-1],
218
+ widget_middleware.font,
219
+ widget_middleware.font_size,
220
+ )
221
+ ) / 2
222
+
223
+ # Additional comb adjustment for center alignment
224
+ if int(alignment) == 1 and widget_middleware.comb is True and length != 0:
225
+ # Shift left by half the first character's padding
226
+ x -= character_paddings[0] / 2
227
+ if length % 2 == 0:
228
+ # For even-length comb text, shift further left by half the first char width plus padding
229
+ x -= (
230
+ character_paddings[0]
231
+ + stringWidth(
232
+ text_value[:1],
233
+ widget_middleware.font,
234
+ widget_middleware.font_size,
235
+ )
236
+ / 2
237
+ )
238
+
239
+ return x
240
+
241
+
242
+ def calculate_text_coord_y(widget: dict, widget_middleware: Text) -> float:
243
+ """
244
+ Calculate the vertical (y) coordinate for text drawing within a PDF form field.
245
+
246
+ This function determines the y-coordinate based on:
247
+ - The widget's rectangle height
248
+ - The font size
249
+ - Whether the field is multiline (which shifts text closer to the top)
250
+
251
+ Args:
252
+ widget: PDF form widget dictionary containing Rect info.
253
+ widget_middleware: Text middleware containing font size and multiline info.
254
+
255
+ Returns:
256
+ float: The calculated y-coordinate for the text baseline.
257
+ """
258
+
259
+ # Convert font size to PDF points (font size is in pixels, 96 dpi to 72 dpi)
260
+ string_height = widget_middleware.font_size * 96 / 72
261
+
262
+ # Calculate vertical midpoint of the widget rectangle
263
+ height_mid_point = (float(widget[Rect][1]) + float(widget[Rect][3])) / 2
264
+
265
+ # Default y: vertically center the text baseline within the widget
266
+ # This formula centers the text height around the vertical midpoint
267
+ y = (height_mid_point - string_height / 2 + height_mid_point) / 2
268
+
269
+ # If multiline, position baseline closer to the top edge for better appearance
270
+ if is_text_multiline(widget):
271
+ y = float(widget[Rect][3]) - string_height / 1.5
272
+
273
+ return y
274
+
275
+
110
276
  def get_draw_text_coordinates(
111
277
  widget: dict, widget_middleware: Text
112
278
  ) -> Tuple[Union[float, int], Union[float, int]]:
113
- """Returns coordinates to draw text at given a PDF form text widget."""
279
+ """
280
+ Calculate the (x, y) coordinates for drawing text within a PDF form field.
281
+
282
+ This function determines the starting position for rendering text,
283
+ taking into account:
284
+ - Preview mode (which offsets text above the field)
285
+ - Text length and wrapping
286
+ - Alignment (left, center, right)
287
+ - Comb formatting and character paddings
288
+ - Multiline adjustments for vertical position
289
+
290
+ Args:
291
+ widget: PDF form widget dictionary containing Rect and alignment info.
292
+ widget_middleware: Text middleware containing font, comb, and text properties.
293
+
294
+ Returns:
295
+ Tuple[Union[float, int], Union[float, int]]: The (x, y) coordinates
296
+ for the text baseline starting point.
297
+ """
114
298
 
299
+ # If preview mode, draw slightly above the top boundary for visibility
115
300
  if widget_middleware.preview:
116
301
  return (
117
302
  float(widget[Rect][0]),
118
303
  float(widget[Rect][3]) + 5,
119
304
  )
120
305
 
306
+ # Prepare text value, respecting max length
121
307
  text_value = widget_middleware.value or ""
122
308
  length = (
123
309
  min(len(text_value), widget_middleware.max_length)
@@ -126,66 +312,24 @@ def get_draw_text_coordinates(
126
312
  )
127
313
  text_value = text_value[:length]
128
314
 
315
+ # Further trim text if wrapping is enabled
129
316
  if widget_middleware.text_wrap_length is not None:
130
317
  text_value = text_value[: widget_middleware.text_wrap_length]
131
318
 
319
+ # Prepare character paddings for comb fields
132
320
  character_paddings = (
133
321
  widget_middleware.character_paddings[:length]
134
322
  if widget_middleware.character_paddings is not None
135
323
  else widget_middleware.character_paddings
136
324
  )
137
325
 
138
- alignment = (
139
- extract_widget_property(widget, WIDGET_ALIGNMENT_PATTERNS, None, int) or 0
326
+ # Calculate horizontal position based on alignment and comb settings
327
+ x = calculate_text_coord_x(
328
+ widget, widget_middleware, text_value, length, character_paddings
140
329
  )
141
- x = float(widget[Rect][0])
142
-
143
- if int(alignment) != 0:
144
- width_mid_point = (float(widget[Rect][0]) + float(widget[Rect][2])) / 2
145
- string_width = stringWidth(
146
- text_value,
147
- widget_middleware.font,
148
- widget_middleware.font_size,
149
- )
150
- if widget_middleware.comb is True and length:
151
- string_width = character_paddings[-1] + stringWidth(
152
- text_value[-1],
153
- widget_middleware.font,
154
- widget_middleware.font_size,
155
- )
156
330
 
157
- if int(alignment) == 1:
158
- x = width_mid_point - string_width / 2
159
- elif int(alignment) == 2:
160
- x = float(widget[Rect][2]) - string_width
161
- if length > 0 and widget_middleware.comb is True:
162
- x -= (
163
- get_char_rect_width(widget, widget_middleware)
164
- - stringWidth(
165
- text_value[-1],
166
- widget_middleware.font,
167
- widget_middleware.font_size,
168
- )
169
- ) / 2
170
-
171
- string_height = widget_middleware.font_size * 96 / 72
172
- height_mid_point = (float(widget[Rect][1]) + float(widget[Rect][3])) / 2
173
- y = (height_mid_point - string_height / 2 + height_mid_point) / 2
174
- if is_text_multiline(widget):
175
- y = float(widget[Rect][3]) - string_height / 1.5
176
-
177
- if int(alignment) == 1 and widget_middleware.comb is True and length != 0:
178
- x -= character_paddings[0] / 2
179
- if length % 2 == 0:
180
- x -= (
181
- character_paddings[0]
182
- + stringWidth(
183
- text_value[:1],
184
- widget_middleware.font,
185
- widget_middleware.font_size,
186
- )
187
- / 2
188
- )
331
+ # Calculate vertical position based on font size and multiline
332
+ y = calculate_text_coord_y(widget, widget_middleware)
189
333
 
190
334
  return x, y
191
335
 
@@ -193,9 +337,15 @@ def get_draw_text_coordinates(
193
337
  def get_text_line_x_coordinates(
194
338
  widget: dict, widget_middleware: Text
195
339
  ) -> Union[List[float], None]:
196
- """
197
- Returns the x coordinates to draw lines
198
- of the text at given a PDF form paragraph widget.
340
+ """Calculates x-coordinates for each line in a multiline text field.
341
+
342
+ Args:
343
+ widget: PDF form widget dictionary
344
+ widget_middleware: Text middleware with text_lines property
345
+
346
+ Returns:
347
+ Union[List[float], None]: List of x-coordinates for each text line,
348
+ or None if not a multiline field
199
349
  """
200
350
 
201
351
  if (
@@ -220,7 +370,16 @@ def get_text_line_x_coordinates(
220
370
  def generate_coordinate_grid(
221
371
  pdf: bytes, color: Tuple[float, float, float], margin: float
222
372
  ) -> bytes:
223
- """Creates a grid view for the coordinates of a PDF."""
373
+ """Generates a coordinate grid overlay for a PDF document.
374
+
375
+ Args:
376
+ pdf: Input PDF document as bytes
377
+ color: RGB tuple (0-1 range) for grid line color
378
+ margin: Spacing between grid lines in PDF points
379
+
380
+ Returns:
381
+ bytes: New PDF with grid overlay as byte stream
382
+ """
224
383
 
225
384
  pdf_file = PdfReader(stream_to_io(pdf))
226
385
  lines_by_page = {}
PyPDFForm/filler.py CHANGED
@@ -1,5 +1,16 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Contains helpers for filling a PDF form."""
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
9
+
10
+ The main functions are:
11
+ - fill(): Uses watermark technique for complex form filling
12
+ - simple_fill(): Directly modifies form fields for simpler cases
13
+ """
3
14
 
4
15
  from io import BytesIO
5
16
  from typing import Dict, Tuple, Union, cast
@@ -35,7 +46,20 @@ from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
35
46
  def check_radio_handler(
36
47
  widget: dict, middleware: Union[Checkbox, Radio], radio_button_tracker: dict
37
48
  ) -> Tuple[Text, Union[float, int], Union[float, int], bool]:
38
- """Handles draw parameters for checkbox and radio button widgets."""
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
+ """
39
63
 
40
64
  font_size = (
41
65
  checkbox_radio_font_size(widget) if middleware.size is None else middleware.size
@@ -60,7 +84,16 @@ def check_radio_handler(
60
84
  def signature_image_handler(
61
85
  widget: dict, middleware: Union[Signature, Image], images_to_draw: list
62
86
  ) -> bool:
63
- """Handles draw parameters for signature and image widgets."""
87
+ """Prepares image data for signature and image widgets.
88
+
89
+ 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
93
+
94
+ Returns:
95
+ bool: True if an image needs to be drawn, False otherwise
96
+ """
64
97
 
65
98
  stream = middleware.stream
66
99
  any_image_to_draw = False
@@ -86,7 +119,19 @@ def signature_image_handler(
86
119
  def text_handler(
87
120
  widget: dict, middleware: Text
88
121
  ) -> Tuple[Text, Union[float, int], Union[float, int], bool]:
89
- """Handles draw parameters for text field widgets."""
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
+ """
90
135
 
91
136
  middleware.text_line_x_coordinates = get_text_line_x_coordinates(widget, middleware)
92
137
  x, y = get_draw_text_coordinates(widget, middleware)
@@ -103,7 +148,15 @@ def border_handler(
103
148
  ellipse_borders_to_draw: list,
104
149
  line_borders_to_draw: list,
105
150
  ) -> None:
106
- """Handles draw parameters for each widget's border."""
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
+ """
107
160
 
108
161
  if (
109
162
  isinstance(middleware, Radio)
@@ -136,7 +189,16 @@ def border_handler(
136
189
 
137
190
 
138
191
  def get_drawn_stream(to_draw: dict, stream: bytes, action: str) -> bytes:
139
- """Generates a stream of an input PDF stream with stuff drawn on it."""
192
+ """Applies drawing operations to a PDF stream.
193
+
194
+ Args:
195
+ to_draw: Dictionary mapping page numbers to drawing parameters
196
+ stream: Input PDF as bytes
197
+ action: Type of drawing operation ('text', 'image', 'rect', etc.)
198
+
199
+ Returns:
200
+ bytes: Modified PDF with drawings applied
201
+ """
140
202
 
141
203
  watermark_list = []
142
204
  for page, stuffs in to_draw.items():
@@ -153,7 +215,20 @@ def fill(
153
215
  template_stream: bytes,
154
216
  widgets: Dict[str, WIDGET_TYPES],
155
217
  ) -> bytes:
156
- """Fills a PDF using watermarks."""
218
+ """Fills a PDF form using watermark technique for complex rendering.
219
+
220
+ This method:
221
+ - Handles text, images, borders for all widget types
222
+ - Preserves original form fields while adding visual elements
223
+ - Supports complex cases like multiline text and image scaling
224
+
225
+ Args:
226
+ template_stream: Input PDF form as bytes
227
+ widgets: Dictionary mapping field names to widget middleware
228
+
229
+ Returns:
230
+ bytes: Filled PDF form as bytes
231
+ """
157
232
 
158
233
  texts_to_draw = {}
159
234
  images_to_draw = {}
@@ -226,7 +301,12 @@ def fill(
226
301
 
227
302
 
228
303
  def enable_adobe_mode(pdf: PdfReader, adobe_mode: bool) -> None:
229
- """Enables Adobe mode so that texts filled can show up in Acrobat."""
304
+ """Configures PDF for Adobe Acrobat compatibility.
305
+
306
+ Args:
307
+ pdf: PdfReader instance of the PDF
308
+ adobe_mode: If True, sets NeedAppearances flag for Acrobat
309
+ """
230
310
 
231
311
  if adobe_mode and AcroForm in pdf.trailer[Root]:
232
312
  pdf.trailer[Root][AcroForm].update(
@@ -240,7 +320,22 @@ def simple_fill(
240
320
  flatten: bool = False,
241
321
  adobe_mode: bool = False,
242
322
  ) -> bytes:
243
- """Fills a PDF form in place."""
323
+ """Fills a PDF form by directly modifying form fields.
324
+
325
+ This method:
326
+ - Updates field values directly in the PDF
327
+ - Supports flattening to make fields read-only
328
+ - Works with Adobe Acrobat compatibility mode
329
+
330
+ Args:
331
+ template: Input PDF form as bytes
332
+ widgets: Dictionary mapping field names to widget middleware
333
+ flatten: If True, makes form fields read-only
334
+ adobe_mode: If True, enables Adobe Acrobat compatibility
335
+
336
+ Returns:
337
+ bytes: Filled PDF form as bytes
338
+ """
244
339
 
245
340
  pdf = PdfReader(stream_to_io(template))
246
341
  enable_adobe_mode(pdf, adobe_mode)