PyPDFForm 4.7.8__tar.gz → 4.7.10__tar.gz

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.
Files changed (79) hide show
  1. {pypdfform-4.7.8 → pypdfform-4.7.10}/PKG-INFO +4 -2
  2. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/__init__.py +1 -1
  3. pypdfform-4.7.10/PyPDFForm/cli/__init__.py +172 -0
  4. pypdfform-4.7.10/PyPDFForm/cli/coordinate.py +80 -0
  5. pypdfform-4.7.10/PyPDFForm/cli/update.py +45 -0
  6. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/base.py +1 -0
  7. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/template.py +13 -5
  8. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/watermark.py +1 -3
  9. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/base.py +3 -3
  10. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/signature.py +1 -4
  11. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/PKG-INFO +4 -2
  12. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/SOURCES.txt +5 -0
  13. pypdfform-4.7.10/PyPDFForm.egg-info/entry_points.txt +2 -0
  14. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/requires.txt +4 -1
  15. {pypdfform-4.7.8 → pypdfform-4.7.10}/pyproject.toml +8 -5
  16. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_bulk_create_fields.py +67 -1
  17. pypdfform-4.7.10/tests/test_cli.py +47 -0
  18. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_functional.py +6 -0
  19. {pypdfform-4.7.8 → pypdfform-4.7.10}/LICENSE +0 -0
  20. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/__init__.py +0 -0
  21. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/adapter.py +0 -0
  22. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/__init__.py +0 -0
  23. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/base.py +0 -0
  24. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/link.py +0 -0
  25. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/stamp.py +0 -0
  26. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/text.py +0 -0
  27. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  28. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/assets/__init__.py +0 -0
  29. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/assets/bedrock.py +0 -0
  30. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/assets/blank.py +0 -0
  31. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/constants.py +0 -0
  32. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/coordinate.py +0 -0
  33. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/deprecation.py +0 -0
  34. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/egress.py +0 -0
  35. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/filler.py +0 -0
  36. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/font.py +0 -0
  37. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/hooks.py +0 -0
  38. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/image.py +0 -0
  39. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/__init__.py +0 -0
  40. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  41. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  42. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/image.py +0 -0
  43. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/radio.py +0 -0
  44. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/signature.py +0 -0
  45. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/text.py +0 -0
  46. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/patterns.py +0 -0
  47. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/__init__.py +0 -0
  48. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/circle.py +0 -0
  49. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/ellipse.py +0 -0
  50. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/image.py +0 -0
  51. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/line.py +0 -0
  52. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/rect.py +0 -0
  53. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/text.py +0 -0
  54. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/types.py +0 -0
  55. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/utils.py +0 -0
  56. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/__init__.py +0 -0
  57. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  58. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  59. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/image.py +0 -0
  60. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/radio.py +0 -0
  61. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/text.py +0 -0
  62. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/wrapper.py +0 -0
  63. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  64. {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/top_level.txt +0 -0
  65. {pypdfform-4.7.8 → pypdfform-4.7.10}/README.md +0 -0
  66. {pypdfform-4.7.8 → pypdfform-4.7.10}/setup.cfg +0 -0
  67. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_create_widget.py +0 -0
  68. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_draw_elements.py +0 -0
  69. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_dropdown.py +0 -0
  70. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_extract_middleware_attributes.py +0 -0
  71. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_fill_max_length_text_field.py +0 -0
  72. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_font_widths.py +0 -0
  73. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_generate_appearance_streams.py +0 -0
  74. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_js.py +0 -0
  75. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_need_appearances.py +0 -0
  76. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_paragraph.py +0 -0
  77. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_signature.py +0 -0
  78. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_use_full_widget_name.py +0 -0
  79. {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.7.8
3
+ Version: 4.7.10
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -24,8 +24,10 @@ Requires-Dist: cryptography<47.0.0,>=46.0.3
24
24
  Requires-Dist: fonttools<5.0.0,>=4.60.1
25
25
  Requires-Dist: pikepdf<11.0.0,>=10.5.0
26
26
  Requires-Dist: pillow<13.0.0,>=12.0.0
27
- Requires-Dist: pypdf<7.0.0,>=6.9.0
27
+ Requires-Dist: pypdf<6.10.0,>=6.9.0
28
28
  Requires-Dist: reportlab<5.0.0,>=4.4.6
29
+ Provides-Extra: cli
30
+ Requires-Dist: typer<1.0.0,>=0.24.1; extra == "cli"
29
31
  Provides-Extra: dev
30
32
  Requires-Dist: black<27.0.0,>=25.11.0; extra == "dev"
31
33
  Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
@@ -22,7 +22,7 @@ PyPDFForm aims to simplify PDF form manipulation, making it accessible to develo
22
22
 
23
23
  import logging
24
24
 
25
- __version__ = "4.7.8"
25
+ __version__ = "4.7.10"
26
26
 
27
27
  from .lib.annotations import Annotations
28
28
  from .lib.assets.blank import BlankPage
@@ -0,0 +1,172 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module provides the command-line interface for PyPDFForm.
4
+
5
+ It defines the CLI application using Typer, providing commands for
6
+ interacting with PyPDFForm functionality from the terminal.
7
+ """
8
+
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from .. import __version__
14
+ from .coordinate import coordinate_cli
15
+ from .update import update_cli
16
+
17
+ cli_app = typer.Typer(
18
+ context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
19
+ )
20
+ cli_app.add_typer(
21
+ coordinate_cli,
22
+ name="coordinate",
23
+ help="Subcommands for interacting with PDF coordinates and dimensions.",
24
+ )
25
+ cli_app.add_typer(
26
+ update_cli,
27
+ name="update",
28
+ help="Subcommands for updating PDF files and their elements.",
29
+ )
30
+
31
+
32
+ def version_callback(value: bool) -> None:
33
+ """
34
+ Callback function to handle the version option.
35
+
36
+ This is triggered when the --version or -v flag is passed to the CLI.
37
+ It prints the current version of PyPDFForm and exits the application.
38
+
39
+ Args:
40
+ value (bool): The value passed to the version option. If True,
41
+ the version information is displayed and the application exits.
42
+ """
43
+ if value:
44
+ print(f"v{__version__}")
45
+ raise typer.Exit
46
+
47
+
48
+ def need_appearances_callback(ctx: typer.Context, value: bool) -> None:
49
+ """
50
+ Callback function to handle the need_appearances option.
51
+
52
+ This is triggered when the --need-appearances flag is passed to the CLI.
53
+ It stores the value in the context object for use by subcommands.
54
+
55
+ Args:
56
+ ctx (typer.Context): The Typer context object used to pass data
57
+ between callbacks and commands.
58
+ value (bool): The value passed to the need_appearances option.
59
+ If True, PDF viewers will be instructed to generate appearance
60
+ streams for the output.
61
+ """
62
+ if not ctx.obj:
63
+ ctx.obj = {}
64
+ ctx.obj["need_appearances"] = value
65
+
66
+
67
+ def generate_appearance_streams_callback(ctx: typer.Context, value: bool) -> None:
68
+ """
69
+ Callback function to handle the generate_appearance_streams option.
70
+
71
+ This is triggered when the --generate-appearance-streams flag is passed
72
+ to the CLI. It stores the value in the context object for use by subcommands.
73
+
74
+ Args:
75
+ ctx (typer.Context): The Typer context object used to pass data
76
+ between callbacks and commands.
77
+ value (bool): The value passed to the generate_appearance_streams
78
+ option. If True, appearance streams will be explicitly generated
79
+ for all form fields in output PDFs using pikepdf.
80
+ """
81
+ if not ctx.obj:
82
+ ctx.obj = {}
83
+ ctx.obj["generate_appearance_streams"] = value
84
+
85
+
86
+ def preserve_metadata_callback(ctx: typer.Context, value: bool) -> None:
87
+ """
88
+ Callback function to handle the preserve_metadata option.
89
+
90
+ This is triggered when the --preserve-metadata flag is passed to the CLI.
91
+ It stores the value in the context object for use by subcommands.
92
+
93
+ Args:
94
+ ctx (typer.Context): The Typer context object used to pass data
95
+ between callbacks and commands.
96
+ value (bool): The value passed to the preserve_metadata option.
97
+ If True, metadata will be preserved in output PDFs.
98
+ """
99
+ if not ctx.obj:
100
+ ctx.obj = {}
101
+ ctx.obj["preserve_metadata"] = value
102
+
103
+
104
+ def use_full_widget_name_callback(ctx: typer.Context, value: bool) -> None:
105
+ """
106
+ Callback function to handle the use_full_widget_name option.
107
+
108
+ This is triggered when the --use-full-widget-name flag is passed to the CLI.
109
+ It stores the value in the context object for use by subcommands.
110
+
111
+ Args:
112
+ ctx (typer.Context): The Typer context object used to pass data
113
+ between callbacks and commands.
114
+ value (bool): The value passed to the use_full_widget_name option.
115
+ If True, fully qualified names (including parent field names)
116
+ will be used when looking up form fields.
117
+ """
118
+ if not ctx.obj:
119
+ ctx.obj = {}
120
+ ctx.obj["use_full_widget_name"] = value
121
+
122
+
123
+ @cli_app.callback(invoke_without_command=True, help="PyPDFForm command-line interface.")
124
+ def main(
125
+ version: Annotated[ # pylint: disable=W0613
126
+ bool,
127
+ typer.Option(
128
+ "--version",
129
+ "-v",
130
+ callback=version_callback,
131
+ is_eager=True,
132
+ help="Show the current version of the CLI and exit.",
133
+ ),
134
+ ] = False,
135
+ need_appearances: Annotated[ # pylint: disable=W0613
136
+ bool,
137
+ typer.Option(
138
+ "--need-appearances",
139
+ callback=need_appearances_callback,
140
+ help="Instruct PDF viewers to generate appearance streams for any output PDF.",
141
+ ),
142
+ ] = False,
143
+ generate_appearance_streams: Annotated[ # pylint: disable=W0613
144
+ bool,
145
+ typer.Option(
146
+ "--generate-appearance-streams",
147
+ callback=generate_appearance_streams_callback,
148
+ help="Generate appearance streams for any output PDF.",
149
+ ),
150
+ ] = False,
151
+ preserve_metadata: Annotated[ # pylint: disable=W0613
152
+ bool,
153
+ typer.Option(
154
+ "--preserve-metadata",
155
+ callback=preserve_metadata_callback,
156
+ help="Preserve PDF metadata in the output.",
157
+ ),
158
+ ] = False,
159
+ use_full_widget_name: Annotated[ # pylint: disable=W0613
160
+ bool,
161
+ typer.Option(
162
+ "--use-full-widget-name",
163
+ callback=use_full_widget_name_callback,
164
+ help="Use fully qualified names when accessing form fields.",
165
+ ),
166
+ ] = False,
167
+ ) -> None:
168
+ # pylint: disable=C0116
169
+ ...
170
+
171
+
172
+ __all__ = ["cli_app"]
@@ -0,0 +1,80 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ CLI commands for interacting with PDF coordinates.
4
+
5
+ This module provides command-line interface commands for working with
6
+ PDF coordinates and dimensions, such as generating a coordinate grid view.
7
+ """
8
+
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from .. import PdfWrapper
14
+
15
+ coordinate_cli = typer.Typer(
16
+ context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
17
+ )
18
+
19
+
20
+ @coordinate_cli.command(no_args_is_help=True)
21
+ def grid(
22
+ ctx: typer.Context,
23
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
24
+ output: Annotated[
25
+ str,
26
+ typer.Option(
27
+ "--output",
28
+ "-o",
29
+ help="Path to save the output PDF. Defaults to the original path if not specified.",
30
+ ),
31
+ ] = None,
32
+ red: Annotated[
33
+ float,
34
+ typer.Option(
35
+ "--red",
36
+ "-r",
37
+ help="Red channel of the RGB color.",
38
+ ),
39
+ ] = None,
40
+ green: Annotated[
41
+ float,
42
+ typer.Option(
43
+ "--green",
44
+ "-g",
45
+ help="Green channel of the RGB color.",
46
+ ),
47
+ ] = None,
48
+ blue: Annotated[
49
+ float,
50
+ typer.Option(
51
+ "--blue",
52
+ "-b",
53
+ help="Blue channel of the RGB color.",
54
+ ),
55
+ ] = None,
56
+ margin: Annotated[
57
+ float,
58
+ typer.Option(
59
+ "--margin",
60
+ "-m",
61
+ help="Margin of the grid view in points.",
62
+ ),
63
+ ] = None,
64
+ ) -> None:
65
+ """
66
+ Generate a coordinate grid view for a PDF.
67
+ """
68
+ params = {}
69
+ if any(
70
+ [
71
+ red is not None,
72
+ green is not None,
73
+ blue is not None,
74
+ ]
75
+ ):
76
+ params["color"] = (red or 0, green or 0, blue or 0)
77
+
78
+ if margin is not None:
79
+ params["margin"] = int(margin) if margin.is_integer() else margin
80
+ PdfWrapper(pdf, **ctx.obj).generate_coordinate_grid(**params).write(output or pdf)
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ CLI commands for updating PDFs.
4
+
5
+ This module provides command-line interface commands for modifying
6
+ PDF files, such as updating metadata or other elements.
7
+ These commands allow users to update PDF documents directly from
8
+ the terminal without needing to write Python code.
9
+
10
+ The commands in this module wrap the functionality provided by
11
+ the PdfWrapper class, exposing it through a Typer-based CLI for
12
+ ease of use.
13
+ """
14
+
15
+ from typing import Annotated
16
+
17
+ import typer
18
+
19
+ from .. import PdfWrapper
20
+
21
+ update_cli = typer.Typer(
22
+ context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
23
+ )
24
+
25
+
26
+ @update_cli.command(no_args_is_help=True)
27
+ def title(
28
+ ctx: typer.Context,
29
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
30
+ new_title: Annotated[
31
+ str, typer.Option("--title", "-t", help="The new title for the PDF file.")
32
+ ],
33
+ output: Annotated[
34
+ str,
35
+ typer.Option(
36
+ "--output",
37
+ "-o",
38
+ help="Path to save the output PDF. Defaults to the original path if not specified.",
39
+ ),
40
+ ] = None,
41
+ ) -> None:
42
+ """
43
+ Update the title of a PDF file.
44
+ """
45
+ PdfWrapper(pdf, title=new_title, **ctx.obj).write(output or pdf)
@@ -62,6 +62,7 @@ class Widget:
62
62
  self.hooks_to_trigger: list = []
63
63
 
64
64
  # coordinate & dimension
65
+ self.page_number: Optional[int] = None
65
66
  self.x: Optional[float | List[float]] = None
66
67
  self.y: Optional[float | List[float]] = None
67
68
  self.width: Optional[float | List[float]] = None
@@ -73,28 +73,32 @@ def build_widgets(
73
73
  """
74
74
  results = {}
75
75
 
76
- for widgets in get_widgets_by_page(pdf_stream).values():
76
+ for page_num, widgets in get_widgets_by_page(pdf_stream).items():
77
77
  for widget in widgets:
78
- _process_widget(widget, use_full_widget_name, results)
78
+ _process_widget(widget, page_num, use_full_widget_name, results)
79
79
 
80
80
  return results
81
81
 
82
82
 
83
83
  def _process_widget(
84
- widget: dict, use_full_widget_name: bool, results: Dict[str, WIDGET_TYPES]
84
+ widget: dict,
85
+ page_number: int,
86
+ use_full_widget_name: bool,
87
+ results: Dict[str, WIDGET_TYPES],
85
88
  ) -> None:
86
89
  """
87
90
  Processes a single widget and adds it to the results dictionary.
88
91
 
89
92
  Args:
90
93
  widget (dict): The widget dictionary from the PDF.
94
+ page_number (int): The 1-indexed page number the widget appears on.
91
95
  use_full_widget_name (bool): Whether to use the full widget name.
92
96
  results (Dict[str, WIDGET_TYPES]): The dictionary of widgets being built.
93
97
  """
94
98
  key = get_widget_key(widget, use_full_widget_name)
95
99
  _widget = construct_widget(widget, key)
96
100
  if _widget is not None:
97
- _populate_common_properties(widget, _widget)
101
+ _populate_common_properties(widget, page_number, _widget)
98
102
 
99
103
  if isinstance(_widget, Text):
100
104
  _populate_text_properties(widget, _widget)
@@ -111,15 +115,19 @@ def _process_widget(
111
115
  results[key] = _widget
112
116
 
113
117
 
114
- def _populate_common_properties(widget: dict, _widget: WIDGET_TYPES) -> None:
118
+ def _populate_common_properties(
119
+ widget: dict, page_number: int, _widget: WIDGET_TYPES
120
+ ) -> None:
115
121
  """
116
122
  Populates common properties for a widget.
117
123
 
118
124
  Args:
119
125
  widget (dict): The widget dictionary from the PDF.
126
+ page_number (int): The 1-indexed page number the widget appears on.
120
127
  _widget (WIDGET_TYPES): The widget object to populate.
121
128
  """
122
129
  # widget property extractions don't trigger hooks in this function
130
+ _widget.__dict__["page_number"] = page_number
123
131
  _widget.__dict__["tooltip"] = extract_widget_property(
124
132
  widget, WIDGET_DESCRIPTION_PATTERNS, None, str
125
133
  )
@@ -260,7 +260,6 @@ def create_watermarks_and_draw(
260
260
  page_to_to_draw[each["page_number"]].append(each)
261
261
 
262
262
  pdf_file = PdfReader(BytesIO(pdf))
263
- buff = BytesIO()
264
263
 
265
264
  for i, page in enumerate(pdf_file.pages):
266
265
  elements = page_to_to_draw[i + 1]
@@ -268,8 +267,7 @@ def create_watermarks_and_draw(
268
267
  result.append(b"")
269
268
  continue
270
269
 
271
- buff.seek(0)
272
- buff.flush()
270
+ buff = BytesIO()
273
271
 
274
272
  canvas = Canvas(
275
273
  buff,
@@ -177,7 +177,6 @@ class Widget:
177
177
  result = []
178
178
 
179
179
  pdf = PdfReader(BytesIO(stream))
180
- watermark = BytesIO()
181
180
 
182
181
  widgets_by_page = {}
183
182
  for widget in widgets:
@@ -191,8 +190,9 @@ class Widget:
191
190
  result.append(b"")
192
191
  continue
193
192
 
194
- watermark.seek(0)
195
- watermark.flush()
193
+ # Use a fresh buffer per page to avoid stale trailing bytes
194
+ # when the current page watermark is smaller than a previous page.
195
+ watermark = BytesIO()
196
196
 
197
197
  canvas = Canvas(
198
198
  watermark,
@@ -120,12 +120,9 @@ class SignatureWidget:
120
120
  key = get_widget_key(annot.get_object(), False)
121
121
  annot_type_to_annot[key] = annot.get_object()
122
122
 
123
- watermark = BytesIO()
124
-
125
123
  for i, p in enumerate(input_pdf.pages):
126
124
  # pylint: disable=R0801
127
- watermark.seek(0)
128
- watermark.flush()
125
+ watermark = BytesIO()
129
126
  canvas = Canvas(
130
127
  watermark,
131
128
  pagesize=(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.7.8
3
+ Version: 4.7.10
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -24,8 +24,10 @@ Requires-Dist: cryptography<47.0.0,>=46.0.3
24
24
  Requires-Dist: fonttools<5.0.0,>=4.60.1
25
25
  Requires-Dist: pikepdf<11.0.0,>=10.5.0
26
26
  Requires-Dist: pillow<13.0.0,>=12.0.0
27
- Requires-Dist: pypdf<7.0.0,>=6.9.0
27
+ Requires-Dist: pypdf<6.10.0,>=6.9.0
28
28
  Requires-Dist: reportlab<5.0.0,>=4.4.6
29
+ Provides-Extra: cli
30
+ Requires-Dist: typer<1.0.0,>=0.24.1; extra == "cli"
29
31
  Provides-Extra: dev
30
32
  Requires-Dist: black<27.0.0,>=25.11.0; extra == "dev"
31
33
  Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
@@ -5,8 +5,12 @@ PyPDFForm/__init__.py
5
5
  PyPDFForm.egg-info/PKG-INFO
6
6
  PyPDFForm.egg-info/SOURCES.txt
7
7
  PyPDFForm.egg-info/dependency_links.txt
8
+ PyPDFForm.egg-info/entry_points.txt
8
9
  PyPDFForm.egg-info/requires.txt
9
10
  PyPDFForm.egg-info/top_level.txt
11
+ PyPDFForm/cli/__init__.py
12
+ PyPDFForm/cli/coordinate.py
13
+ PyPDFForm/cli/update.py
10
14
  PyPDFForm/lib/__init__.py
11
15
  PyPDFForm/lib/adapter.py
12
16
  PyPDFForm/lib/constants.py
@@ -56,6 +60,7 @@ PyPDFForm/lib/widgets/radio.py
56
60
  PyPDFForm/lib/widgets/signature.py
57
61
  PyPDFForm/lib/widgets/text.py
58
62
  tests/test_bulk_create_fields.py
63
+ tests/test_cli.py
59
64
  tests/test_create_widget.py
60
65
  tests/test_draw_elements.py
61
66
  tests/test_dropdown.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pypdfform = PyPDFForm.cli:cli_app
@@ -2,9 +2,12 @@ cryptography<47.0.0,>=46.0.3
2
2
  fonttools<5.0.0,>=4.60.1
3
3
  pikepdf<11.0.0,>=10.5.0
4
4
  pillow<13.0.0,>=12.0.0
5
- pypdf<7.0.0,>=6.9.0
5
+ pypdf<6.10.0,>=6.9.0
6
6
  reportlab<5.0.0,>=4.4.6
7
7
 
8
+ [cli]
9
+ typer<1.0.0,>=0.24.1
10
+
8
11
  [dev]
9
12
  black<27.0.0,>=25.11.0
10
13
  coverage<8.0.0,>=7.12.0
@@ -7,9 +7,7 @@ name = "PyPDFForm"
7
7
  dynamic = ["version"]
8
8
  description = "The Python library for PDF forms."
9
9
  readme = "README.md"
10
- authors = [
11
- {name = "Jinge Li"}
12
- ]
10
+ authors = [{ name = "Jinge Li" }]
13
11
  license = "MIT"
14
12
  license-files = ["LICENSE"]
15
13
  classifiers = [
@@ -31,7 +29,7 @@ dependencies = [
31
29
  "fonttools>=4.60.1,<5.0.0",
32
30
  "pikepdf>=10.5.0,<11.0.0",
33
31
  "pillow>=12.0.0,<13.0.0",
34
- "pypdf>=6.9.0,<7.0.0",
32
+ "pypdf>=6.9.0,<6.10.0", # TODO: revert this
35
33
  "reportlab>=4.4.6,<5.0.0",
36
34
  ]
37
35
 
@@ -40,6 +38,7 @@ Homepage = "https://github.com/chinapandaman/PyPDFForm"
40
38
  Documentation = "https://chinapandaman.github.io/PyPDFForm/"
41
39
 
42
40
  [project.optional-dependencies]
41
+ cli = ["typer>=0.24.1,<1.0.0"]
43
42
  dev = [
44
43
  "black>=25.11.0,<27.0.0",
45
44
  "coverage>=7.12.0,<8.0.0",
@@ -56,6 +55,9 @@ dev = [
56
55
  "ruff>=0.14.6,<1.0.0",
57
56
  ]
58
57
 
58
+ [project.scripts]
59
+ pypdfform = "PyPDFForm.cli:cli_app"
60
+
59
61
  [tool.coverage.run]
60
62
  include = ["PyPDFForm/*"]
61
63
 
@@ -129,7 +131,7 @@ reportAssignmentType = "error"
129
131
  reportOptionalSubscript = "error"
130
132
 
131
133
  [tool.setuptools.dynamic]
132
- version = {attr = "PyPDFForm.__version__"}
134
+ version = { attr = "PyPDFForm.__version__" }
133
135
 
134
136
  [tool.setuptools.packages.find]
135
137
  include = ["PyPDFForm*"]
@@ -137,4 +139,5 @@ include = ["PyPDFForm*"]
137
139
  [tool.pytest.ini_options]
138
140
  markers = [
139
141
  "posix_only", # mainly because of zlib vs zlib-ng
142
+ "cli_test",
140
143
  ]
@@ -1,10 +1,13 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  import os
4
+ from io import BytesIO
4
5
 
5
6
  import pytest
7
+ from pypdf import PdfReader
6
8
 
7
- from PyPDFForm import Fields, PdfWrapper
9
+ from PyPDFForm import BlankPage, Fields, PdfWrapper
10
+ from PyPDFForm.lib.constants import Annots, Subtype, Widget
8
11
 
9
12
 
10
13
  @pytest.mark.posix_only
@@ -59,6 +62,21 @@ def test_bulk_create_fields_stress_max(pdf_samples, request):
59
62
  obj += PdfWrapper(os.path.join(pdf_samples, "dummy.pdf"))
60
63
  obj.bulk_create_fields(fields)
61
64
 
65
+ prefix_to_page = {
66
+ "text_": 1,
67
+ "check_": 2,
68
+ "dropdown_": 3,
69
+ "radio_": 4,
70
+ "image_": 5,
71
+ "signature_": 6,
72
+ }
73
+
74
+ for k, v in obj.widgets.items():
75
+ for each, page in prefix_to_page.items():
76
+ if k.startswith(each):
77
+ assert v.page_number == page
78
+ break
79
+
62
80
  request.config.results["expected_path"] = expected_path
63
81
  request.config.results["stream"] = obj.read()
64
82
 
@@ -144,3 +162,51 @@ def test_bulk_create_fields_stress_max_mixed(pdf_samples, request):
144
162
 
145
163
  assert len(obj.read()) == len(expected)
146
164
  assert obj.read() == expected
165
+
166
+
167
+ def test_bulk_create_fields_does_not_duplicate_widgets():
168
+ wrapper = PdfWrapper(BlankPage() * 2)
169
+
170
+ fields = []
171
+ y = 760.0
172
+ for i in range(35):
173
+ fields.append(
174
+ Fields.TextField(
175
+ f"p1_field_{i}",
176
+ 1,
177
+ 50.0,
178
+ y,
179
+ width=220.0,
180
+ height=16.0,
181
+ font_size=9.0,
182
+ )
183
+ )
184
+ y -= 20.0
185
+
186
+ y = 760.0
187
+ for i in range(4):
188
+ fields.append(
189
+ Fields.TextField(
190
+ f"p2_field_{i}",
191
+ 2,
192
+ 50.0,
193
+ y,
194
+ width=220.0,
195
+ height=16.0,
196
+ font_size=9.0,
197
+ )
198
+ )
199
+ y -= 24.0
200
+
201
+ output = wrapper.bulk_create_fields(fields).read()
202
+ reader = PdfReader(BytesIO(output))
203
+
204
+ widget_count = 0
205
+ for page in reader.pages:
206
+ annots = page.get(Annots) or []
207
+ for annot_ref in annots:
208
+ annot = annot_ref.get_object()
209
+ if str(annot.get(Subtype)) == Widget:
210
+ widget_count += 1
211
+
212
+ assert widget_count == len(fields)
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import pytest
4
+ from typer.testing import CliRunner
5
+
6
+ from PyPDFForm import __version__
7
+ from PyPDFForm.cli import cli_app
8
+
9
+ runner = CliRunner()
10
+
11
+
12
+ @pytest.mark.cli_test
13
+ def test_root_command():
14
+ result = runner.invoke(cli_app)
15
+ assert result.exit_code == 2
16
+
17
+ assert "PyPDFForm command-line interface." in result.output
18
+ assert "Usage:" in result.output
19
+ assert "main" not in result.output
20
+
21
+
22
+ @pytest.mark.cli_test
23
+ def test_root_command_with_version():
24
+ long = runner.invoke(cli_app, ["--version"])
25
+ short = runner.invoke(cli_app, ["-v"])
26
+
27
+ assert long.exit_code == 0
28
+ assert short.exit_code == 0
29
+
30
+ assert long.output == f"v{__version__}\n"
31
+ assert long.output == short.output
32
+
33
+
34
+ @pytest.mark.cli_test
35
+ def test_coordinate_command():
36
+ result = runner.invoke(cli_app, ["coordinate"])
37
+ assert result.exit_code == 2
38
+
39
+ assert "Usage:" in result.output
40
+
41
+
42
+ @pytest.mark.cli_test
43
+ def test_update_command():
44
+ result = runner.invoke(cli_app, ["update"])
45
+ assert result.exit_code == 2
46
+
47
+ assert "Usage:" in result.output
@@ -747,31 +747,37 @@ def test_widget_coord_resolution():
747
747
  ]
748
748
  )
749
749
 
750
+ assert obj.widgets["text"].page_number == 1
750
751
  assert obj.widgets["text"].x == 50
751
752
  assert obj.widgets["text"].y == 100
752
753
  assert obj.widgets["text"].width == 200
753
754
  assert obj.widgets["text"].height == 150
754
755
 
756
+ assert obj.widgets["check"].page_number == 1
755
757
  assert obj.widgets["check"].x == 150
756
758
  assert obj.widgets["check"].y == 200
757
759
  assert obj.widgets["check"].width == 60
758
760
  assert obj.widgets["check"].height == 60
759
761
 
762
+ assert obj.widgets["radio"].page_number == 1
760
763
  assert obj.widgets["radio"].x == [400, 500, 600]
761
764
  assert obj.widgets["radio"].y == [450, 550, 650]
762
765
  assert obj.widgets["radio"].width == 10
763
766
  assert obj.widgets["radio"].height == 10
764
767
 
768
+ assert obj.widgets["dropdown"].page_number == 1
765
769
  assert obj.widgets["dropdown"].x == 400
766
770
  assert obj.widgets["dropdown"].y == 100
767
771
  assert obj.widgets["dropdown"].width == 250
768
772
  assert obj.widgets["dropdown"].height == 200
769
773
 
774
+ assert obj.widgets["image"].page_number == 1
770
775
  assert obj.widgets["image"].x == 300
771
776
  assert obj.widgets["image"].y == 400
772
777
  assert obj.widgets["image"].width == 400
773
778
  assert obj.widgets["image"].height == 300
774
779
 
780
+ assert obj.widgets["signature"].page_number == 1
775
781
  assert obj.widgets["signature"].x == 500
776
782
  assert obj.widgets["signature"].y == 600
777
783
  assert obj.widgets["signature"].width == 600
File without changes
File without changes
File without changes
File without changes