PyPDFForm 4.7.9__tar.gz → 4.8.0__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 (82) hide show
  1. {pypdfform-4.7.9 → pypdfform-4.8.0}/PKG-INFO +7 -7
  2. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/cli/__init__.py +67 -4
  4. pypdfform-4.8.0/PyPDFForm/cli/coordinate.py +165 -0
  5. pypdfform-4.8.0/PyPDFForm/cli/create.py +169 -0
  6. pypdfform-4.8.0/PyPDFForm/cli/inspect.py +53 -0
  7. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/cli/update.py +7 -8
  8. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/middleware/base.py +1 -0
  9. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/template.py +13 -5
  10. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm.egg-info/PKG-INFO +7 -7
  11. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm.egg-info/SOURCES.txt +3 -0
  12. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm.egg-info/requires.txt +6 -6
  13. {pypdfform-4.7.9 → pypdfform-4.8.0}/pyproject.toml +7 -6
  14. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_bulk_create_fields.py +15 -0
  15. pypdfform-4.8.0/tests/test_cli.py +71 -0
  16. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_functional.py +6 -0
  17. pypdfform-4.7.9/tests/test_cli.py +0 -35
  18. {pypdfform-4.7.9 → pypdfform-4.8.0}/LICENSE +0 -0
  19. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/__init__.py +0 -0
  20. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/adapter.py +0 -0
  21. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/annotations/__init__.py +0 -0
  22. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/annotations/base.py +0 -0
  23. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/annotations/link.py +0 -0
  24. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/annotations/stamp.py +0 -0
  25. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/annotations/text.py +0 -0
  26. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  27. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/assets/__init__.py +0 -0
  28. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/assets/bedrock.py +0 -0
  29. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/assets/blank.py +0 -0
  30. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/constants.py +0 -0
  31. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/coordinate.py +0 -0
  32. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/deprecation.py +0 -0
  33. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/egress.py +0 -0
  34. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/filler.py +0 -0
  35. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/font.py +0 -0
  36. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/hooks.py +0 -0
  37. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/image.py +0 -0
  38. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/middleware/__init__.py +0 -0
  39. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  40. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  41. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/middleware/image.py +0 -0
  42. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/middleware/radio.py +0 -0
  43. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/middleware/signature.py +0 -0
  44. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/middleware/text.py +0 -0
  45. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/patterns.py +0 -0
  46. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/raw/__init__.py +0 -0
  47. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/raw/circle.py +0 -0
  48. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/raw/ellipse.py +0 -0
  49. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/raw/image.py +0 -0
  50. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/raw/line.py +0 -0
  51. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/raw/rect.py +0 -0
  52. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/raw/text.py +0 -0
  53. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/types.py +0 -0
  54. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/utils.py +0 -0
  55. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/watermark.py +0 -0
  56. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/widgets/__init__.py +0 -0
  57. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/widgets/base.py +0 -0
  58. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  59. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  60. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/widgets/image.py +0 -0
  61. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/widgets/radio.py +0 -0
  62. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/widgets/signature.py +0 -0
  63. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/widgets/text.py +0 -0
  64. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm/lib/wrapper.py +0 -0
  65. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  66. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm.egg-info/entry_points.txt +0 -0
  67. {pypdfform-4.7.9 → pypdfform-4.8.0}/PyPDFForm.egg-info/top_level.txt +0 -0
  68. {pypdfform-4.7.9 → pypdfform-4.8.0}/README.md +0 -0
  69. {pypdfform-4.7.9 → pypdfform-4.8.0}/setup.cfg +0 -0
  70. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_create_widget.py +0 -0
  71. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_draw_elements.py +0 -0
  72. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_dropdown.py +0 -0
  73. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_extract_middleware_attributes.py +0 -0
  74. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_fill_max_length_text_field.py +0 -0
  75. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_font_widths.py +0 -0
  76. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_generate_appearance_streams.py +0 -0
  77. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_js.py +0 -0
  78. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_need_appearances.py +0 -0
  79. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_paragraph.py +0 -0
  80. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_signature.py +0 -0
  81. {pypdfform-4.7.9 → pypdfform-4.8.0}/tests/test_use_full_widget_name.py +0 -0
  82. {pypdfform-4.7.9 → pypdfform-4.8.0}/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.9
3
+ Version: 4.8.0
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -21,26 +21,26 @@ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: cryptography<47.0.0,>=46.0.3
24
- Requires-Dist: fonttools<5.0.0,>=4.60.1
24
+ Requires-Dist: fonttools<5.0.0,>=4.62.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<7.0.0,>=6.10.1
28
28
  Requires-Dist: reportlab<5.0.0,>=4.4.6
29
29
  Provides-Extra: cli
30
30
  Requires-Dist: typer<1.0.0,>=0.24.1; extra == "cli"
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: black<27.0.0,>=25.11.0; extra == "dev"
33
33
  Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
34
- Requires-Dist: isort<9.0.0,>=7.0.0; extra == "dev"
35
- Requires-Dist: jsonschema<5.0.0,>=4.25.1; extra == "dev"
34
+ Requires-Dist: isort<9.0.0,>=8.0.1; extra == "dev"
35
+ Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "dev"
36
36
  Requires-Dist: mike<3.0.0,>=2.1.3; extra == "dev"
37
37
  Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
38
38
  Requires-Dist: mkdocs-material<10.0.0,>=9.7.0; extra == "dev"
39
39
  Requires-Dist: pudb<2026.0.0,>=2025.1.3; extra == "dev"
40
40
  Requires-Dist: pylint<5.0.0,>=4.0.3; extra == "dev"
41
41
  Requires-Dist: pyright<2.0.0,>=1.1.407; extra == "dev"
42
- Requires-Dist: pytest<10.0.0,>=9.0.1; extra == "dev"
43
- Requires-Dist: requests<3.0.0,>=2.32.5; extra == "dev"
42
+ Requires-Dist: pytest<10.0.0,>=9.0.3; extra == "dev"
43
+ Requires-Dist: requests<3.0.0,>=2.33.1; extra == "dev"
44
44
  Requires-Dist: ruff<1.0.0,>=0.14.6; extra == "dev"
45
45
  Dynamic: license-file
46
46
 
@@ -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.9"
25
+ __version__ = "4.8.0"
26
26
 
27
27
  from .lib.annotations import Annotations
28
28
  from .lib.assets.blank import BlankPage
@@ -6,16 +6,35 @@ It defines the CLI application using Typer, providing commands for
6
6
  interacting with PyPDFForm functionality from the terminal.
7
7
  """
8
8
 
9
+ import json
9
10
  from typing import Annotated
10
11
 
11
12
  import typer
12
13
 
13
- from .. import __version__
14
+ from .. import PdfWrapper, Widgets, __version__
15
+ from .coordinate import coordinate_cli
16
+ from .create import create_cli
17
+ from .inspect import inspect_cli
14
18
  from .update import update_cli
15
19
 
16
20
  cli_app = typer.Typer(
17
21
  context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
18
22
  )
23
+ cli_app.add_typer(
24
+ coordinate_cli,
25
+ name="coordinate",
26
+ help="Subcommands for interacting with PDF coordinates and dimensions.",
27
+ )
28
+ cli_app.add_typer(
29
+ create_cli,
30
+ name="create",
31
+ help="Subcommands for creating elements on PDF forms.",
32
+ )
33
+ cli_app.add_typer(
34
+ inspect_cli,
35
+ name="inspect",
36
+ help="Subcommands for inspecting PDF forms.",
37
+ )
19
38
  cli_app.add_typer(
20
39
  update_cli,
21
40
  name="update",
@@ -114,7 +133,7 @@ def use_full_widget_name_callback(ctx: typer.Context, value: bool) -> None:
114
133
  ctx.obj["use_full_widget_name"] = value
115
134
 
116
135
 
117
- @cli_app.callback(invoke_without_command=True, help="Welcome to the PyPDFForm CLI!")
136
+ @cli_app.callback(invoke_without_command=True, help="PyPDFForm command-line interface.")
118
137
  def main(
119
138
  version: Annotated[ # pylint: disable=W0613
120
139
  bool,
@@ -123,7 +142,7 @@ def main(
123
142
  "-v",
124
143
  callback=version_callback,
125
144
  is_eager=True,
126
- help="Show current version of the CLI and exit.",
145
+ help="Show the current version of the CLI and exit.",
127
146
  ),
128
147
  ] = False,
129
148
  need_appearances: Annotated[ # pylint: disable=W0613
@@ -147,7 +166,7 @@ def main(
147
166
  typer.Option(
148
167
  "--preserve-metadata",
149
168
  callback=preserve_metadata_callback,
150
- help="Preserve PDF metadata in output.",
169
+ help="Preserve PDF metadata in the output.",
151
170
  ),
152
171
  ] = False,
153
172
  use_full_widget_name: Annotated[ # pylint: disable=W0613
@@ -163,4 +182,48 @@ def main(
163
182
  ...
164
183
 
165
184
 
185
+ @cli_app.command(no_args_is_help=True)
186
+ def fill(
187
+ ctx: typer.Context,
188
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
189
+ data: Annotated[
190
+ str,
191
+ typer.Option(
192
+ "--file",
193
+ "-f",
194
+ help="Path to the JSON file representing the filling data.",
195
+ ),
196
+ ],
197
+ output: Annotated[
198
+ str,
199
+ typer.Option(
200
+ "--output",
201
+ "-o",
202
+ help="Path to save the output PDF. Defaults to the original path if not specified.",
203
+ ),
204
+ ] = None,
205
+ flatten: Annotated[
206
+ bool,
207
+ typer.Option(
208
+ "--flatten", help="Whether to flatten the filled PDF form or not."
209
+ ),
210
+ ] = None,
211
+ ) -> None:
212
+ """
213
+ Fill a PDF form.
214
+ """
215
+ with open(data, "r", encoding="utf-8") as f:
216
+ input_data = json.load(f)
217
+
218
+ obj = PdfWrapper(pdf, **ctx.obj)
219
+ for k, each in obj.widgets.items():
220
+ if k in input_data and isinstance(each, (Widgets.Image, Widgets.Signature)):
221
+ each.preserve_aspect_ratio = input_data.get(k, {}).get(
222
+ "preserve_aspect_ratio", each.preserve_aspect_ratio
223
+ )
224
+ input_data[k] = input_data[k]["path"]
225
+
226
+ obj.fill(input_data, flatten=flatten).write(output or pdf)
227
+
228
+
166
229
  __all__ = ["cli_app"]
@@ -0,0 +1,165 @@
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
+ import json
10
+ from typing import Annotated
11
+
12
+ import typer
13
+
14
+ from .. import PdfWrapper
15
+
16
+ coordinate_cli = typer.Typer(
17
+ context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
18
+ )
19
+
20
+
21
+ @coordinate_cli.command(no_args_is_help=True)
22
+ def grid(
23
+ ctx: typer.Context,
24
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
25
+ output: Annotated[
26
+ str,
27
+ typer.Option(
28
+ "--output",
29
+ "-o",
30
+ help="Path to save the output PDF. Defaults to the original path if not specified.",
31
+ ),
32
+ ] = None,
33
+ red: Annotated[
34
+ float,
35
+ typer.Option(
36
+ "--red",
37
+ "-r",
38
+ help="Red channel of the RGB color.",
39
+ ),
40
+ ] = None,
41
+ green: Annotated[
42
+ float,
43
+ typer.Option(
44
+ "--green",
45
+ "-g",
46
+ help="Green channel of the RGB color.",
47
+ ),
48
+ ] = None,
49
+ blue: Annotated[
50
+ float,
51
+ typer.Option(
52
+ "--blue",
53
+ "-b",
54
+ help="Blue channel of the RGB color.",
55
+ ),
56
+ ] = None,
57
+ margin: Annotated[
58
+ float,
59
+ typer.Option(
60
+ "--margin",
61
+ "-m",
62
+ help="Margin of the grid view in points.",
63
+ ),
64
+ ] = None,
65
+ ) -> None:
66
+ """
67
+ Generate a coordinate grid view for a PDF.
68
+ """
69
+ params = {}
70
+ if any(
71
+ [
72
+ red is not None,
73
+ green is not None,
74
+ blue is not None,
75
+ ]
76
+ ):
77
+ params["color"] = (red or 0, green or 0, blue or 0)
78
+
79
+ if margin is not None:
80
+ params["margin"] = int(margin) if margin.is_integer() else margin
81
+ PdfWrapper(pdf, **ctx.obj).generate_coordinate_grid(**params).write(output or pdf)
82
+
83
+
84
+ @coordinate_cli.command(no_args_is_help=True)
85
+ def inspect(
86
+ ctx: typer.Context,
87
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
88
+ field: Annotated[
89
+ str, typer.Option("--field", "-f", help="Name of the form field to inspect.")
90
+ ],
91
+ ) -> None:
92
+ """
93
+ Inspect the page number, coordinates, and dimensions of a form field's rectangular bounding box.
94
+ """
95
+ f = PdfWrapper(pdf, **ctx.obj).widgets[field]
96
+
97
+ print(
98
+ json.dumps(
99
+ {
100
+ "page_number": f.page_number,
101
+ "x": f.x,
102
+ "y": f.y,
103
+ "width": f.width,
104
+ "height": f.height,
105
+ }
106
+ )
107
+ )
108
+
109
+
110
+ @coordinate_cli.command(no_args_is_help=True)
111
+ def modify(
112
+ ctx: typer.Context,
113
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
114
+ field: Annotated[
115
+ str, typer.Option("--field", "-f", help="Name of the form field to modify.")
116
+ ],
117
+ output: Annotated[
118
+ str,
119
+ typer.Option(
120
+ "--output",
121
+ "-o",
122
+ help="Path to save the output PDF. Defaults to the original path if not specified.",
123
+ ),
124
+ ] = None,
125
+ x: Annotated[
126
+ float,
127
+ typer.Option(
128
+ "--x",
129
+ help="New x coordinate.",
130
+ ),
131
+ ] = None,
132
+ y: Annotated[
133
+ float,
134
+ typer.Option(
135
+ "--y",
136
+ help="New y coordinate.",
137
+ ),
138
+ ] = None,
139
+ width: Annotated[
140
+ float,
141
+ typer.Option(
142
+ "--width",
143
+ help="New width.",
144
+ ),
145
+ ] = None,
146
+ height: Annotated[
147
+ float,
148
+ typer.Option(
149
+ "--height",
150
+ help="New height.",
151
+ ),
152
+ ] = None,
153
+ ) -> None:
154
+ """
155
+ Modify the coordinates and dimensions of a form field's rectangular bounding box.
156
+ """
157
+ obj = PdfWrapper(pdf, **ctx.obj)
158
+ f = obj.widgets[field]
159
+
160
+ f.x = x if x is not None else f.x
161
+ f.y = y if y is not None else f.y
162
+ f.width = width if width is not None else f.width
163
+ f.height = height if height is not None else f.height
164
+
165
+ obj.write(output or pdf)
@@ -0,0 +1,169 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ CLI module for creating PDF form fields.
4
+
5
+ This module provides command-line interfaces to create various types of PDF form fields
6
+ (such as text fields, checkboxes, radio buttons, dropdowns, signatures, and images)
7
+ in an existing PDF. It aims to mimic the field creation features available via the
8
+ Python API as described in the preparation documentation.
9
+ """
10
+
11
+ import json
12
+ from typing import Annotated
13
+
14
+ import typer
15
+
16
+ from .. import BlankPage, Fields, PdfWrapper, RawElements
17
+
18
+ create_cli = typer.Typer(
19
+ context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
20
+ )
21
+
22
+
23
+ def _create_elements_from_file(
24
+ pdf: str,
25
+ data: str,
26
+ element_map: dict,
27
+ method_name: str,
28
+ ctx: typer.Context,
29
+ output: str = None,
30
+ ) -> None:
31
+ """
32
+ Create PDF elements from a JSON file.
33
+
34
+ Args:
35
+ pdf: Path to the input PDF file.
36
+ data: Path to the JSON file containing element parameters.
37
+ element_map: Mapping of element type names to element classes.
38
+ method_name: Name of the method to call on PdfWrapper (e.g., "bulk_create_fields", "draw").
39
+ ctx: Typer context containing configuration options.
40
+ output: Path to save the output PDF. Defaults to the original path if not specified.
41
+ """
42
+ with open(data, "r", encoding="utf-8") as f:
43
+ input_data = json.load(f)
44
+
45
+ obj = PdfWrapper(pdf, **ctx.obj)
46
+ ungrouped_input = []
47
+ registered_font = {}
48
+ for k, v in input_data.items():
49
+ for each in v:
50
+ if "font" in each:
51
+ if each["font"] not in registered_font:
52
+ font_name = f"new_font_{len(registered_font)}"
53
+ obj.register_font(font_name, each["font"])
54
+ registered_font[each["font"]] = font_name
55
+ each["font"] = registered_font[each["font"]]
56
+ ungrouped_input.append(element_map[k](**each))
57
+
58
+ getattr(obj, method_name)(ungrouped_input).write(output or pdf)
59
+
60
+
61
+ @create_cli.command(no_args_is_help=True)
62
+ def field(
63
+ ctx: typer.Context,
64
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
65
+ data: Annotated[
66
+ str,
67
+ typer.Option(
68
+ "--file",
69
+ "-f",
70
+ help="Path to the JSON file representing the field creation parameters.",
71
+ ),
72
+ ],
73
+ output: Annotated[
74
+ str,
75
+ typer.Option(
76
+ "--output",
77
+ "-o",
78
+ help="Path to save the output PDF. Defaults to the original path if not specified.",
79
+ ),
80
+ ] = None,
81
+ ) -> None:
82
+ """
83
+ Create PDF form fields.
84
+ """
85
+ field_map = {
86
+ "text": Fields.TextField,
87
+ "check": Fields.CheckBoxField,
88
+ "radio": Fields.RadioGroup,
89
+ "dropdown": Fields.DropdownField,
90
+ "image": Fields.ImageField,
91
+ "signature": Fields.SignatureField,
92
+ }
93
+ _create_elements_from_file(pdf, data, field_map, "bulk_create_fields", ctx, output)
94
+
95
+
96
+ @create_cli.command(no_args_is_help=True)
97
+ def raw(
98
+ ctx: typer.Context,
99
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
100
+ data: Annotated[
101
+ str,
102
+ typer.Option(
103
+ "--file",
104
+ "-f",
105
+ help="Path to the JSON file representing the draw parameters.",
106
+ ),
107
+ ],
108
+ output: Annotated[
109
+ str,
110
+ typer.Option(
111
+ "--output",
112
+ "-o",
113
+ help="Path to save the output PDF. Defaults to the original path if not specified.",
114
+ ),
115
+ ] = None,
116
+ ) -> None:
117
+ """
118
+ Draw raw PDF elements.
119
+ """
120
+ raw_element_map = {
121
+ "text": RawElements.RawText,
122
+ "image": RawElements.RawImage,
123
+ "line": RawElements.RawLine,
124
+ "rectangle": RawElements.RawRectangle,
125
+ "circle": RawElements.RawCircle,
126
+ "ellipse": RawElements.RawEllipse,
127
+ }
128
+ _create_elements_from_file(pdf, data, raw_element_map, "draw", ctx, output)
129
+
130
+
131
+ @create_cli.command(no_args_is_help=True)
132
+ def blank(
133
+ ctx: typer.Context,
134
+ output: Annotated[
135
+ str,
136
+ typer.Option(
137
+ "--output",
138
+ "-o",
139
+ help="Path to save the output PDF.",
140
+ ),
141
+ ],
142
+ count: Annotated[
143
+ int, typer.Option("--count", "-c", help="Number of blank pages.")
144
+ ] = None,
145
+ width: Annotated[
146
+ float,
147
+ typer.Option(
148
+ "--width",
149
+ help="Width of the blank PDF.",
150
+ ),
151
+ ] = None,
152
+ height: Annotated[
153
+ float, typer.Option("--height", help="Height of the blank PDF.")
154
+ ] = None,
155
+ ) -> None:
156
+ """
157
+ Create a new blank PDF.
158
+ """
159
+ params = {}
160
+ if width is not None:
161
+ params["width"] = width
162
+ if height is not None:
163
+ params["height"] = height
164
+
165
+ obj = BlankPage(**params)
166
+ if count is not None and count > 1:
167
+ obj = BlankPage(**params) * count
168
+
169
+ PdfWrapper(obj, **ctx.obj).write(output)
@@ -0,0 +1,53 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ CLI commands for inspecting PDF form field data.
4
+
5
+ This module provides command-line interface commands for extracting
6
+ information from PDF forms. Features include generating a JSON schema
7
+ describing the form fields, inspecting the current filled data of a
8
+ PDF form, and generating sample data for filling a form.
9
+ """
10
+
11
+ import json
12
+ from typing import Annotated
13
+
14
+ import typer
15
+
16
+ from .. import PdfWrapper
17
+
18
+ inspect_cli = typer.Typer(
19
+ context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
20
+ )
21
+
22
+
23
+ @inspect_cli.command(no_args_is_help=True)
24
+ def schema(
25
+ ctx: typer.Context,
26
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
27
+ ) -> None:
28
+ """
29
+ Generate a JSON schema that describes a PDF form.
30
+ """
31
+ print(json.dumps(PdfWrapper(pdf, **ctx.obj).schema))
32
+
33
+
34
+ @inspect_cli.command(no_args_is_help=True)
35
+ def data(
36
+ ctx: typer.Context,
37
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
38
+ ) -> None:
39
+ """
40
+ Inspect the current filled data of a PDF form.
41
+ """
42
+ print(json.dumps(PdfWrapper(pdf, **ctx.obj).data))
43
+
44
+
45
+ @inspect_cli.command(no_args_is_help=True)
46
+ def sample(
47
+ ctx: typer.Context,
48
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
49
+ ) -> None:
50
+ """
51
+ Generate sample data for filling a PDF form.
52
+ """
53
+ print(json.dumps(PdfWrapper(pdf, **ctx.obj).sample_data))
@@ -1,12 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
- # TODO: fix this docstring
3
2
  """
4
- CLI commands for updating PDF metadata.
3
+ CLI commands for updating PDFs.
5
4
 
6
5
  This module provides command-line interface commands for modifying
7
- PDF file metadata such as title, author, subject, and other properties.
6
+ PDF files, such as updating metadata or other elements.
8
7
  These commands allow users to update PDF documents directly from
9
- the terminal without needing to use Python code.
8
+ the terminal without needing to write Python code.
10
9
 
11
10
  The commands in this module wrap the functionality provided by
12
11
  the PdfWrapper class, exposing it through a Typer-based CLI for
@@ -27,20 +26,20 @@ update_cli = typer.Typer(
27
26
  @update_cli.command(no_args_is_help=True)
28
27
  def title(
29
28
  ctx: typer.Context,
30
- pdf: Annotated[str, typer.Argument(help="The local path to a PDF.")],
29
+ pdf: Annotated[str, typer.Argument(help="Path to the input PDF file.")],
31
30
  new_title: Annotated[
32
- str, typer.Option("--title", "-t", help="The new title for the PDF.")
31
+ str, typer.Option("--title", "-t", help="The new title for the PDF file.")
33
32
  ],
34
33
  output: Annotated[
35
34
  str,
36
35
  typer.Option(
37
36
  "--output",
38
37
  "-o",
39
- help="The location to save the PDF to. Defaults to the original path if unspecified.",
38
+ help="Path to save the output PDF. Defaults to the original path if not specified.",
40
39
  ),
41
40
  ] = None,
42
41
  ) -> None:
43
42
  """
44
- Update the title of a PDF.
43
+ Update the title of a PDF file.
45
44
  """
46
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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.7.9
3
+ Version: 4.8.0
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -21,26 +21,26 @@ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: cryptography<47.0.0,>=46.0.3
24
- Requires-Dist: fonttools<5.0.0,>=4.60.1
24
+ Requires-Dist: fonttools<5.0.0,>=4.62.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<7.0.0,>=6.10.1
28
28
  Requires-Dist: reportlab<5.0.0,>=4.4.6
29
29
  Provides-Extra: cli
30
30
  Requires-Dist: typer<1.0.0,>=0.24.1; extra == "cli"
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: black<27.0.0,>=25.11.0; extra == "dev"
33
33
  Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
34
- Requires-Dist: isort<9.0.0,>=7.0.0; extra == "dev"
35
- Requires-Dist: jsonschema<5.0.0,>=4.25.1; extra == "dev"
34
+ Requires-Dist: isort<9.0.0,>=8.0.1; extra == "dev"
35
+ Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "dev"
36
36
  Requires-Dist: mike<3.0.0,>=2.1.3; extra == "dev"
37
37
  Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
38
38
  Requires-Dist: mkdocs-material<10.0.0,>=9.7.0; extra == "dev"
39
39
  Requires-Dist: pudb<2026.0.0,>=2025.1.3; extra == "dev"
40
40
  Requires-Dist: pylint<5.0.0,>=4.0.3; extra == "dev"
41
41
  Requires-Dist: pyright<2.0.0,>=1.1.407; extra == "dev"
42
- Requires-Dist: pytest<10.0.0,>=9.0.1; extra == "dev"
43
- Requires-Dist: requests<3.0.0,>=2.32.5; extra == "dev"
42
+ Requires-Dist: pytest<10.0.0,>=9.0.3; extra == "dev"
43
+ Requires-Dist: requests<3.0.0,>=2.33.1; extra == "dev"
44
44
  Requires-Dist: ruff<1.0.0,>=0.14.6; extra == "dev"
45
45
  Dynamic: license-file
46
46
 
@@ -9,6 +9,9 @@ PyPDFForm.egg-info/entry_points.txt
9
9
  PyPDFForm.egg-info/requires.txt
10
10
  PyPDFForm.egg-info/top_level.txt
11
11
  PyPDFForm/cli/__init__.py
12
+ PyPDFForm/cli/coordinate.py
13
+ PyPDFForm/cli/create.py
14
+ PyPDFForm/cli/inspect.py
12
15
  PyPDFForm/cli/update.py
13
16
  PyPDFForm/lib/__init__.py
14
17
  PyPDFForm/lib/adapter.py
@@ -1,8 +1,8 @@
1
1
  cryptography<47.0.0,>=46.0.3
2
- fonttools<5.0.0,>=4.60.1
2
+ fonttools<5.0.0,>=4.62.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<7.0.0,>=6.10.1
6
6
  reportlab<5.0.0,>=4.4.6
7
7
 
8
8
  [cli]
@@ -11,14 +11,14 @@ typer<1.0.0,>=0.24.1
11
11
  [dev]
12
12
  black<27.0.0,>=25.11.0
13
13
  coverage<8.0.0,>=7.12.0
14
- isort<9.0.0,>=7.0.0
15
- jsonschema<5.0.0,>=4.25.1
14
+ isort<9.0.0,>=8.0.1
15
+ jsonschema<5.0.0,>=4.26.0
16
16
  mike<3.0.0,>=2.1.3
17
17
  mkdocs<2.0.0,>=1.6.1
18
18
  mkdocs-material<10.0.0,>=9.7.0
19
19
  pudb<2026.0.0,>=2025.1.3
20
20
  pylint<5.0.0,>=4.0.3
21
21
  pyright<2.0.0,>=1.1.407
22
- pytest<10.0.0,>=9.0.1
23
- requests<3.0.0,>=2.32.5
22
+ pytest<10.0.0,>=9.0.3
23
+ requests<3.0.0,>=2.33.1
24
24
  ruff<1.0.0,>=0.14.6
@@ -26,10 +26,10 @@ classifiers = [
26
26
  requires-python = ">=3.10"
27
27
  dependencies = [
28
28
  "cryptography>=46.0.3,<47.0.0",
29
- "fonttools>=4.60.1,<5.0.0",
29
+ "fonttools>=4.62.1,<5.0.0",
30
30
  "pikepdf>=10.5.0,<11.0.0",
31
31
  "pillow>=12.0.0,<13.0.0",
32
- "pypdf>=6.9.0,<7.0.0",
32
+ "pypdf>=6.10.1,<7.0.0",
33
33
  "reportlab>=4.4.6,<5.0.0",
34
34
  ]
35
35
 
@@ -42,16 +42,16 @@ cli = ["typer>=0.24.1,<1.0.0"]
42
42
  dev = [
43
43
  "black>=25.11.0,<27.0.0",
44
44
  "coverage>=7.12.0,<8.0.0",
45
- "isort>=7.0.0,<9.0.0",
46
- "jsonschema>=4.25.1,<5.0.0",
45
+ "isort>=8.0.1,<9.0.0",
46
+ "jsonschema>=4.26.0,<5.0.0",
47
47
  "mike>=2.1.3,<3.0.0",
48
48
  "mkdocs>=1.6.1,<2.0.0",
49
49
  "mkdocs-material>=9.7.0,<10.0.0",
50
50
  "pudb>=2025.1.3,<2026.0.0",
51
51
  "pylint>=4.0.3,<5.0.0",
52
52
  "pyright>=1.1.407,<2.0.0",
53
- "pytest>=9.0.1,<10.0.0",
54
- "requests>=2.32.5,<3.0.0",
53
+ "pytest>=9.0.3,<10.0.0",
54
+ "requests>=2.33.1,<3.0.0",
55
55
  "ruff>=0.14.6,<1.0.0",
56
56
  ]
57
57
 
@@ -139,4 +139,5 @@ include = ["PyPDFForm*"]
139
139
  [tool.pytest.ini_options]
140
140
  markers = [
141
141
  "posix_only", # mainly because of zlib vs zlib-ng
142
+ "cli_test",
142
143
  ]
@@ -62,6 +62,21 @@ def test_bulk_create_fields_stress_max(pdf_samples, request):
62
62
  obj += PdfWrapper(os.path.join(pdf_samples, "dummy.pdf"))
63
63
  obj.bulk_create_fields(fields)
64
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
+
65
80
  request.config.results["expected_path"] = expected_path
66
81
  request.config.results["stream"] = obj.read()
67
82
 
@@ -0,0 +1,71 @@
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_fill_command():
36
+ result = runner.invoke(cli_app, ["fill"])
37
+ assert result.exit_code == 2
38
+
39
+ assert "Usage:" in result.output
40
+
41
+
42
+ @pytest.mark.cli_test
43
+ def test_coordinate_command():
44
+ result = runner.invoke(cli_app, ["coordinate"])
45
+ assert result.exit_code == 2
46
+
47
+ assert "Usage:" in result.output
48
+
49
+
50
+ @pytest.mark.cli_test
51
+ def test_create_command():
52
+ result = runner.invoke(cli_app, ["create"])
53
+ assert result.exit_code == 2
54
+
55
+ assert "Usage:" in result.output
56
+
57
+
58
+ @pytest.mark.cli_test
59
+ def test_inspect_command():
60
+ result = runner.invoke(cli_app, ["inspect"])
61
+ assert result.exit_code == 2
62
+
63
+ assert "Usage:" in result.output
64
+
65
+
66
+ @pytest.mark.cli_test
67
+ def test_update_command():
68
+ result = runner.invoke(cli_app, ["update"])
69
+ assert result.exit_code == 2
70
+
71
+ 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
@@ -1,35 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- from typer.testing import CliRunner
4
-
5
- from PyPDFForm import __version__
6
- from PyPDFForm.cli import cli_app
7
-
8
- runner = CliRunner()
9
-
10
-
11
- def test_root_command():
12
- result = runner.invoke(cli_app)
13
- assert result.exit_code == 2
14
-
15
- assert "Welcome to the PyPDFForm CLI!" in result.output
16
- assert "Usage:" in result.output
17
- assert "main" not in result.output
18
-
19
-
20
- def test_root_command_with_version():
21
- long = runner.invoke(cli_app, ["--version"])
22
- short = runner.invoke(cli_app, ["-v"])
23
-
24
- assert long.exit_code == 0
25
- assert short.exit_code == 0
26
-
27
- assert long.output == f"v{__version__}\n"
28
- assert long.output == short.output
29
-
30
-
31
- def test_update_command():
32
- result = runner.invoke(cli_app, ["update"])
33
- assert result.exit_code == 2
34
-
35
- assert "Usage:" in result.output
File without changes
File without changes
File without changes
File without changes