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/hooks.py CHANGED
@@ -1,9 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Module containing hook functions for PDF form widget manipulation.
2
+ """
3
+ This module defines widget hooks that allow for dynamic modification of PDF form fields.
3
4
 
4
- This module provides functions to apply various transformations and modifications
5
- to PDF form widgets through a hook system. It allows dynamic modification of
6
- widget properties like font sizes and other attributes.
5
+ It provides functions to trigger these hooks, enabling changes to text field properties
6
+ like font, font size, color, alignment, and multiline settings, as well as the size
7
+ of checkbox and radio button widgets. These hooks are triggered during the PDF form
8
+ filling process, allowing for customization of the form's appearance and behavior.
7
9
  """
8
10
 
9
11
  import sys
@@ -15,7 +17,7 @@ from pypdf.generic import (ArrayObject, DictionaryObject, FloatObject,
15
17
  NameObject, NumberObject, TextStringObject)
16
18
 
17
19
  from .constants import (COMB, DA, FONT_COLOR_IDENTIFIER, FONT_SIZE_IDENTIFIER,
18
- MULTILINE, Annots, Ff, Parent, Q, Rect)
20
+ MULTILINE, Annots, Ff, Opt, Parent, Q, Rect)
19
21
  from .template import get_widget_key
20
22
  from .utils import stream_to_io
21
23
 
@@ -25,21 +27,26 @@ def trigger_widget_hooks(
25
27
  widgets: dict,
26
28
  use_full_widget_name: bool,
27
29
  ) -> bytes:
28
- """Apply all registered widget hooks to a PDF document.
30
+ """
31
+ Triggers widget hooks to apply dynamic changes to PDF form fields.
32
+
33
+ This function iterates through the annotations on each page of the PDF and,
34
+ if a widget is associated with an annotation and has hooks to trigger,
35
+ it executes those hooks. Hooks are functions defined in this module that
36
+ modify the annotation dictionary, allowing for dynamic changes to the form field's
37
+ appearance or behavior.
29
38
 
30
39
  Args:
31
- pdf: The input PDF document as bytes
32
- widgets: Dictionary mapping widget names to widget objects
33
- use_full_widget_name: Whether to use full widget names including parent hierarchy
40
+ pdf (bytes): The PDF file data as bytes.
41
+ widgets (dict): A dictionary of widgets, where keys are widget identifiers
42
+ and values are widget objects containing information about the widget
43
+ and its associated hooks.
44
+ use_full_widget_name (bool): Whether to use the full widget name when
45
+ looking up widgets in the widgets dictionary.
34
46
 
35
47
  Returns:
36
- The modified PDF document as bytes
37
-
38
- Note:
39
- This function processes all pages and annotations in the PDF, applying
40
- any hooks registered in the widget objects.
48
+ bytes: The modified PDF data as bytes, with the widget hooks applied.
41
49
  """
42
-
43
50
  pdf_file = PdfReader(stream_to_io(pdf))
44
51
  output = PdfWriter()
45
52
  output.append(pdf_file)
@@ -65,18 +72,45 @@ def trigger_widget_hooks(
65
72
  return f.read()
66
73
 
67
74
 
68
- def update_text_field_font_size(annot: DictionaryObject, val: float) -> None:
69
- """Update the font size of a text field widget.
75
+ def update_text_field_font(annot: DictionaryObject, val: str) -> None:
76
+ """
77
+ Updates the font of a text field annotation.
78
+
79
+ This function modifies the appearance string (DA) in the annotation dictionary
80
+ to change the font used for the text field.
70
81
 
71
82
  Args:
72
- annot: The PDF annotation (widget) dictionary object
73
- val: The new font size value to apply
83
+ annot (DictionaryObject): The annotation dictionary for the text field.
84
+ val (str): The new font name to use for the text field.
85
+ """
86
+ if Parent in annot and DA not in annot:
87
+ text_appearance = annot[Parent][DA]
88
+ else:
89
+ text_appearance = annot[DA]
90
+
91
+ text_appearance = text_appearance.split(" ")
92
+ text_appearance[0] = val
93
+ new_text_appearance = " ".join(text_appearance)
94
+
95
+ if Parent in annot and DA not in annot:
96
+ annot[NameObject(Parent)][NameObject(DA)] = TextStringObject(
97
+ new_text_appearance
98
+ )
99
+ else:
100
+ annot[NameObject(DA)] = TextStringObject(new_text_appearance)
101
+
74
102
 
75
- Note:
76
- Handles both direct font size specification and inherited font sizes
77
- from parent objects. Modifies the DA (default appearance) string.
103
+ def update_text_field_font_size(annot: DictionaryObject, val: float) -> None:
78
104
  """
105
+ Updates the font size of a text field annotation.
106
+
107
+ This function modifies the appearance string (DA) in the annotation dictionary
108
+ to change the font size used for the text field.
79
109
 
110
+ Args:
111
+ annot (DictionaryObject): The annotation dictionary for the text field.
112
+ val (float): The new font size to use for the text field.
113
+ """
80
114
  if Parent in annot and DA not in annot:
81
115
  text_appearance = annot[Parent][DA]
82
116
  else:
@@ -101,18 +135,16 @@ def update_text_field_font_size(annot: DictionaryObject, val: float) -> None:
101
135
 
102
136
 
103
137
  def update_text_field_font_color(annot: DictionaryObject, val: tuple) -> None:
104
- """Update the font color of a text field widget.
138
+ """
139
+ Updates the font color of a text field annotation.
105
140
 
106
- Args:
107
- annot: The PDF annotation (widget) dictionary object
108
- val: Tuple containing RGB color values (e.g. (1, 0, 0) for red)
141
+ This function modifies the appearance string (DA) in the annotation dictionary
142
+ to change the font color used for the text field.
109
143
 
110
- Note:
111
- Handles both direct font color specification and inherited font colors
112
- from parent objects. Modifies the DA (default appearance) string to
113
- include the new color values after the font size identifier.
144
+ Args:
145
+ annot (DictionaryObject): The annotation dictionary for the text field.
146
+ val (tuple): The new font color as an RGB tuple (e.g., (1, 0, 0) for red).
114
147
  """
115
-
116
148
  if Parent in annot and DA not in annot:
117
149
  text_appearance = annot[Parent][DA]
118
150
  else:
@@ -141,76 +173,84 @@ def update_text_field_font_color(annot: DictionaryObject, val: tuple) -> None:
141
173
 
142
174
 
143
175
  def update_text_field_alignment(annot: DictionaryObject, val: int) -> None:
144
- """Update text alignment for text field annotations.
176
+ """
177
+ Updates the text alignment of a text field annotation.
145
178
 
146
- Modifies the alignment (Q) field of a text field annotation to set the
147
- specified text alignment.
179
+ This function modifies the Q entry in the annotation dictionary to change
180
+ the text alignment of the text field.
148
181
 
149
182
  Args:
150
- annot: PDF text field annotation dictionary to modify
151
- val: Alignment value to set (typically 0=left, 1=center, 2=right)
183
+ annot (DictionaryObject): The annotation dictionary for the text field.
184
+ val (int): The new alignment value (0=Left, 1=Center, 2=Right).
152
185
  """
153
-
154
186
  annot[NameObject(Q)] = NumberObject(val)
155
187
 
156
188
 
157
189
  def update_text_field_multiline(annot: DictionaryObject, val: bool) -> None:
158
- """Update multiline flag for text field annotations.
190
+ """
191
+ Updates the multiline property of a text field annotation.
159
192
 
160
- Modifies the field flags (Ff) of a text field annotation to set or
161
- clear the multiline flag based on the input value.
193
+ This function modifies the Ff (flags) entry in the annotation dictionary to
194
+ enable or disable the multiline property of the text field.
162
195
 
163
196
  Args:
164
- annot: PDF text field annotation dictionary to modify
165
- val: Whether to enable multiline (True) or disable (False)
197
+ annot (DictionaryObject): The annotation dictionary for the text field.
198
+ val (bool): True to enable multiline, False to disable.
166
199
  """
167
-
168
200
  if val:
169
201
  annot[NameObject(Ff)] = NumberObject(int(annot[NameObject(Ff)]) | MULTILINE)
170
202
 
171
203
 
172
204
  def update_text_field_comb(annot: DictionaryObject, val: bool) -> None:
173
- """Update comb formatting flag for text field annotations.
205
+ """
206
+ Updates the comb property of a text field annotation.
174
207
 
175
- Modifies the field flags (Ff) of a text field annotation to set or
176
- clear the comb flag which enables/disables comb formatting.
208
+ This function modifies the Ff (flags) entry in the annotation dictionary to
209
+ enable or disable the comb property of the text field, which limits the
210
+ number of characters that can be entered in each line.
177
211
 
178
212
  Args:
179
- annot: PDF text field annotation dictionary to modify
180
- val: Whether to enable comb formatting (True) or disable (False)
213
+ annot (DictionaryObject): The annotation dictionary for the text field.
214
+ val (bool): True to enable comb, False to disable.
181
215
  """
182
-
183
216
  if val:
184
217
  annot[NameObject(Ff)] = NumberObject(int(annot[NameObject(Ff)]) | COMB)
185
218
 
186
219
 
187
220
  def update_check_radio_size(annot: DictionaryObject, val: float) -> None:
188
- """Update the size of a checkbox or radio button widget while maintaining center position.
221
+ """
222
+ Updates the size of a check box or radio button annotation.
189
223
 
190
- Args:
191
- annot: PDF annotation dictionary containing the widget to modify
192
- val: New size value (width and height) for the widget
224
+ This function modifies the Rect entry in the annotation dictionary to change
225
+ the size of the check box or radio button.
193
226
 
194
- Note:
195
- The widget will be resized symmetrically around its center point,
196
- maintaining the same center position while changing its dimensions.
227
+ Args:
228
+ annot (DictionaryObject): The annotation dictionary for the check box or
229
+ radio button.
230
+ val (float): The new size (width and height) for the check box or radio button.
197
231
  """
198
-
199
232
  rect = annot[Rect]
200
- center_x = (rect[0] + rect[2]) / 2
201
- center_y = (rect[1] + rect[3]) / 2
233
+ # scale from bottom left
202
234
  new_rect = [
203
- FloatObject(center_x - val / 2),
204
- FloatObject(center_y - val / 2),
205
- FloatObject(center_x + val / 2),
206
- FloatObject(center_y + val / 2),
235
+ rect[0],
236
+ rect[1],
237
+ FloatObject(rect[0] + val),
238
+ FloatObject(rect[1] + val),
207
239
  ]
208
240
  annot[NameObject(Rect)] = ArrayObject(new_rect)
209
241
 
210
242
 
211
- # TODO: remove this and switch to hooks
212
- NON_ACRO_FORM_PARAM_TO_FUNC = {
213
- ("TextWidget", "alignment"): update_text_field_alignment,
214
- ("TextWidget", "multiline"): update_text_field_multiline,
215
- ("TextWidget", "comb"): update_text_field_comb,
216
- }
243
+ def update_dropdown_choices(annot: DictionaryObject, val: list) -> None:
244
+ """
245
+ Updates the choices in a dropdown field annotation.
246
+
247
+ This function modifies the Opt entry in the annotation dictionary to change
248
+ the available choices in the dropdown field.
249
+
250
+ Args:
251
+ annot (DictionaryObject): The annotation dictionary for the dropdown field.
252
+ val (list): A list of strings representing the new choices for the dropdown.
253
+ """
254
+ annot[NameObject(Opt)] = ArrayObject(
255
+ [ArrayObject([TextStringObject(each), TextStringObject(each)]) for each in val]
256
+ )
PyPDFForm/image.py CHANGED
@@ -1,12 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Provides image processing utilities for PDF forms.
3
-
4
- This module contains functions for:
5
- - Rotating images while maintaining quality
6
- - Extracting image dimensions
7
- - Handling image streams and formats
2
+ """
3
+ This module provides functionalities for handling images within PyPDFForm.
8
4
 
9
- Supports common image formats including JPEG, PNG, and others supported by PIL.
5
+ It includes functions for rotating images, retrieving image dimensions, and
6
+ calculating the resolutions for drawing an image on a PDF page, taking into
7
+ account whether to preserve the aspect ratio.
10
8
  """
11
9
 
12
10
  from io import BytesIO
@@ -14,21 +12,26 @@ from typing import Tuple, Union
14
12
 
15
13
  from PIL import Image
16
14
 
15
+ from .constants import Rect
16
+
17
17
 
18
18
  def rotate_image(image_stream: bytes, rotation: Union[float, int]) -> bytes:
19
- """Rotates an image while maintaining original quality and format.
19
+ """
20
+ Rotates an image by a specified angle in degrees.
21
+
22
+ This function takes an image stream as bytes and rotates it using the PIL library.
23
+ The rotation is performed around the center of the image, and the expand=True
24
+ parameter ensures that the entire rotated image is visible, even if it extends
25
+ beyond the original image boundaries.
20
26
 
21
27
  Args:
22
- image_stream: Input image as bytes
23
- rotation: Rotation angle in degrees (can be a float for precise angles)
28
+ image_stream (bytes): The image data as bytes.
29
+ rotation (Union[float, int]): The rotation angle in degrees. Positive values
30
+ rotate the image counterclockwise, while negative values rotate it clockwise.
24
31
 
25
32
  Returns:
26
- bytes: Rotated image as bytes in the original format
27
-
28
- Note:
29
- Automatically expands the canvas to prevent cropping during rotation.
33
+ bytes: The rotated image data as bytes.
30
34
  """
31
-
32
35
  buff = BytesIO()
33
36
  buff.write(image_stream)
34
37
  buff.seek(0)
@@ -48,18 +51,18 @@ def rotate_image(image_stream: bytes, rotation: Union[float, int]) -> bytes:
48
51
 
49
52
 
50
53
  def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
51
- """Extracts width and height from an image stream.
54
+ """
55
+ Retrieves the width and height of an image from its byte stream.
56
+
57
+ This function uses the PIL library to open the image from the provided byte stream
58
+ and returns its dimensions (width and height) as a tuple of floats.
52
59
 
53
60
  Args:
54
- image_stream: Input image as bytes
61
+ image_stream (bytes): The image data as bytes.
55
62
 
56
63
  Returns:
57
- Tuple[float, float]: (width, height) in pixels
58
-
59
- Note:
60
- Works with any image format supported by PIL (Pillow)
64
+ Tuple[float, float]: The width and height of the image in pixels.
61
65
  """
62
-
63
66
  buff = BytesIO()
64
67
  buff.write(image_stream)
65
68
  buff.seek(0)
@@ -67,3 +70,50 @@ def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
67
70
  image = Image.open(buff)
68
71
 
69
72
  return image.size
73
+
74
+
75
+ def get_draw_image_resolutions(
76
+ widget: dict,
77
+ preserve_aspect_ratio: bool,
78
+ image_width: float,
79
+ image_height: float,
80
+ ) -> Tuple[float, float, float, float]:
81
+ """
82
+ Calculates the position and dimensions for drawing an image on a PDF page.
83
+
84
+ This function determines the x, y coordinates, width, and height for drawing an
85
+ image within a specified widget area on a PDF page. It takes into account whether
86
+ the aspect ratio of the image should be preserved and adjusts the dimensions
87
+ accordingly.
88
+
89
+ Args:
90
+ widget (dict): A dictionary containing the widget's rectangle coordinates
91
+ (x1, y1, x2, y2) under the key "Rect".
92
+ preserve_aspect_ratio (bool): Whether to preserve the aspect ratio of the image.
93
+ If True, the image will be scaled to fit within the widget area while
94
+ maintaining its original aspect ratio.
95
+ image_width (float): The width of the image in pixels.
96
+ image_height (float): The height of the image in pixels.
97
+
98
+ Returns:
99
+ Tuple[float, float, float, float]: A tuple containing the x, y coordinates,
100
+ width, and height of the image to be drawn on the PDF page.
101
+ """
102
+ x = float(widget[Rect][0])
103
+ y = float(widget[Rect][1])
104
+ width = abs(float(widget[Rect][0]) - float(widget[Rect][2]))
105
+ height = abs(float(widget[Rect][1]) - float(widget[Rect][3]))
106
+
107
+ if preserve_aspect_ratio:
108
+ ratio = max(image_width / width, image_height / height)
109
+
110
+ new_width = image_width / ratio
111
+ new_height = image_height / ratio
112
+
113
+ x += abs(new_width - width) / 2
114
+ y += abs(new_height - height) / 2
115
+
116
+ width = new_width
117
+ height = new_height
118
+
119
+ return x, y, width, height
@@ -1,29 +1,24 @@
1
1
  # -*- coding: utf-8 -*-
2
- """Provides base widget middleware for PDF form elements.
3
-
4
- This module contains the Widget base class that defines common functionality
5
- and properties for all PDF form widgets, including:
6
- - Name and value management
7
- - Styling properties (borders, colors)
8
- - Schema generation
9
- - Basic validation
2
+ """
3
+ Module containing base classes for middleware.
4
+
5
+ This module defines the base class for form widgets, which are used to
6
+ represent form fields in a PDF document. The Widget class provides
7
+ common attributes and methods for all form widgets, such as name, value,
8
+ and schema definition.
10
9
  """
11
10
 
12
11
  from typing import Any
13
12
 
14
13
 
15
14
  class Widget:
16
- """Abstract base class for all PDF form widget middleware.
17
-
18
- Provides common interface and functionality for:
19
- - Managing widget names and values
20
- - Handling visual properties (borders, colors)
21
- - Generating JSON schema definitions
22
- - Providing sample values
15
+ """
16
+ Base class for form widget.
23
17
 
24
- Subclasses must implement:
25
- - sample_value property
26
- - Any widget-specific functionality
18
+ The Widget class provides a base implementation for form widgets,
19
+ which are used to represent form fields in a PDF document. It
20
+ defines common attributes and methods for all form widgets, such
21
+ as name, value, and schema definition.
27
22
  """
28
23
 
29
24
  SET_ATTR_TRIGGER_HOOK_MAP = {}
@@ -33,87 +28,76 @@ class Widget:
33
28
  name: str,
34
29
  value: Any = None,
35
30
  ) -> None:
36
- """Initializes a new widget with basic properties.
31
+ """
32
+ Initialize a new widget.
37
33
 
38
34
  Args:
39
- name: Field name/key for the widget
40
- value: Initial value for the widget (default: None)
35
+ name (str): The name of the widget.
36
+ value (Any): The initial value of the widget. Defaults to None.
41
37
  """
42
-
43
38
  super().__init__()
44
39
  self._name = name
45
40
  self._value = value
46
41
  self.desc = None
47
- self.border_color = None
48
- self.background_color = None
49
- self.border_width = None
50
- self.border_style = None
51
- self.dash_array = None
52
- self.render_widget = None
53
42
  self.hooks_to_trigger = []
54
43
 
55
44
  def __setattr__(self, name: str, value: Any) -> None:
56
- """Sets an attribute on the widget with special handling for hook-triggering attributes.
45
+ """
46
+ Set an attribute on the widget.
57
47
 
58
- For attributes listed in SET_ATTR_TRIGGER_HOOK_MAP, when set to non-None values,
59
- adds a hook to hooks_to_trigger list with the mapped hook name and value.
60
- All other attributes are set normally via the standard object.__setattr__.
48
+ This method overrides the default __setattr__ method to
49
+ trigger hooks when certain attributes are set.
61
50
 
62
51
  Args:
63
- name: Name of the attribute to set
64
- value: Value to set the attribute to
65
-
66
- Note:
67
- The hook triggering only occurs when:
68
- 1. The attribute is in SET_ATTR_TRIGGER_HOOK_MAP
69
- 2. The value being set is not None
52
+ name (str): The name of the attribute.
53
+ value (Any): The value of the attribute.
70
54
  """
71
-
72
55
  if name in self.SET_ATTR_TRIGGER_HOOK_MAP and value is not None:
73
56
  self.hooks_to_trigger.append((self.SET_ATTR_TRIGGER_HOOK_MAP[name], value))
74
57
  super().__setattr__(name, value)
75
58
 
76
59
  @property
77
60
  def name(self) -> str:
78
- """Gets the widget's field name/key.
61
+ """
62
+ Get the name of the widget.
79
63
 
80
64
  Returns:
81
- str: The widget's name as it appears in the PDF form
65
+ str: The name of the widget.
82
66
  """
83
-
84
67
  return self._name
85
68
 
86
69
  @property
87
70
  def value(self) -> Any:
88
- """Gets the widget's current value.
71
+ """
72
+ Get the value of the widget.
89
73
 
90
74
  Returns:
91
- Any: The widget's current value (type depends on widget type)
75
+ Any: The value of the widget.
92
76
  """
93
-
94
77
  return self._value
95
78
 
96
79
  @value.setter
97
80
  def value(self, value: Any) -> None:
98
- """Sets the widget's value.
81
+ """
82
+ Set the value of the widget.
99
83
 
100
84
  Args:
101
- value: New value for the widget (type depends on widget type)
85
+ value (Any): The value to set.
102
86
  """
103
-
104
87
  self._value = value
105
88
 
106
89
  @property
107
90
  def schema_definition(self) -> dict:
108
- """Generates a JSON schema definition for the widget.
91
+ """
92
+ Get the schema definition of the widget.
93
+
94
+ This method returns a dictionary that defines the schema
95
+ for the widget. The schema definition is used to validate
96
+ the widget's value.
109
97
 
110
98
  Returns:
111
- dict: Schema properties including:
112
- - description (if available)
113
- - type constraints
114
- - other widget-specific properties
99
+ dict: The schema definition of the widget.
115
100
  """
116
-
117
101
  result = {}
118
102
 
119
103
  if self.desc is not None:
@@ -123,12 +107,10 @@ class Widget:
123
107
 
124
108
  @property
125
109
  def sample_value(self) -> Any:
126
- """Generates a sample value appropriate for the widget type.
127
-
128
- Returns:
129
- Any: A representative value demonstrating the widget's expected input
110
+ """
111
+ Get a sample value for the widget.
130
112
 
131
113
  Raises:
132
- NotImplementedError: Must be implemented by subclasses
114
+ NotImplementedError: This method must be implemented by subclasses.
133
115
  """
134
116
  raise NotImplementedError