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.
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PKG-INFO +4 -2
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/__init__.py +1 -1
- pypdfform-4.7.10/PyPDFForm/cli/__init__.py +172 -0
- pypdfform-4.7.10/PyPDFForm/cli/coordinate.py +80 -0
- pypdfform-4.7.10/PyPDFForm/cli/update.py +45 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/base.py +1 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/template.py +13 -5
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/watermark.py +1 -3
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/base.py +3 -3
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/signature.py +1 -4
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/PKG-INFO +4 -2
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/SOURCES.txt +5 -0
- pypdfform-4.7.10/PyPDFForm.egg-info/entry_points.txt +2 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/requires.txt +4 -1
- {pypdfform-4.7.8 → pypdfform-4.7.10}/pyproject.toml +8 -5
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_bulk_create_fields.py +67 -1
- pypdfform-4.7.10/tests/test_cli.py +47 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_functional.py +6 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/LICENSE +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/egress.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/font.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/patterns.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm/lib/wrapper.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/README.md +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/setup.cfg +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_create_widget.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_draw_elements.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_dropdown.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_font_widths.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_js.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_need_appearances.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_paragraph.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_signature.py +0 -0
- {pypdfform-4.7.8 → pypdfform-4.7.10}/tests/test_use_full_widget_name.py +0 -0
- {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.
|
|
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<
|
|
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"
|
|
@@ -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).
|
|
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,
|
|
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(
|
|
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
|
|
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
|
-
|
|
195
|
-
watermark.
|
|
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
|
|
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.
|
|
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<
|
|
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
|
|
@@ -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<
|
|
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,<
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|