PyPDFForm 4.8.0__tar.gz → 4.8.1__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.8.0 → pypdfform-4.8.1}/PKG-INFO +6 -6
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/__init__.py +1 -1
- pypdfform-4.8.1/PyPDFForm/cli/__init__.py +149 -0
- pypdfform-4.8.1/PyPDFForm/cli/common.py +191 -0
- pypdfform-4.8.1/PyPDFForm/cli/create.py +237 -0
- pypdfform-4.8.1/PyPDFForm/cli/inspect.py +69 -0
- pypdfform-4.8.1/PyPDFForm/cli/update.py +191 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/constants.py +21 -10
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/hooks.py +1 -1
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm.egg-info/PKG-INFO +6 -6
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm.egg-info/SOURCES.txt +1 -2
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm.egg-info/requires.txt +5 -5
- {pypdfform-4.8.0 → pypdfform-4.8.1}/pyproject.toml +5 -5
- pypdfform-4.8.0/PyPDFForm/cli/__init__.py +0 -229
- pypdfform-4.8.0/PyPDFForm/cli/coordinate.py +0 -165
- pypdfform-4.8.0/PyPDFForm/cli/create.py +0 -169
- pypdfform-4.8.0/PyPDFForm/cli/inspect.py +0 -53
- pypdfform-4.8.0/PyPDFForm/cli/update.py +0 -45
- pypdfform-4.8.0/tests/test_cli.py +0 -71
- {pypdfform-4.8.0 → pypdfform-4.8.1}/LICENSE +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/egress.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/font.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/patterns.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/template.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/watermark.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/widgets/base.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/widgets/signature.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm/lib/wrapper.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/README.md +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/setup.cfg +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_create_widget.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_draw_elements.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_dropdown.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_font_widths.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_functional.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_js.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_need_appearances.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_paragraph.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_signature.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/tests/test_use_full_widget_name.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.1}/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.8.
|
|
3
|
+
Version: 4.8.1
|
|
4
4
|
Summary: The Python library for PDF forms.
|
|
5
5
|
Author: Jinge Li
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,21 +23,21 @@ License-File: LICENSE
|
|
|
23
23
|
Requires-Dist: cryptography<47.0.0,>=46.0.3
|
|
24
24
|
Requires-Dist: fonttools<5.0.0,>=4.62.1
|
|
25
25
|
Requires-Dist: pikepdf<11.0.0,>=10.5.0
|
|
26
|
-
Requires-Dist: pillow<13.0.0,>=12.
|
|
26
|
+
Requires-Dist: pillow<13.0.0,>=12.2.0
|
|
27
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
|
-
Requires-Dist: black<27.0.0,>=
|
|
32
|
+
Requires-Dist: black<27.0.0,>=26.3.1; extra == "dev"
|
|
33
33
|
Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
|
|
34
34
|
Requires-Dist: isort<9.0.0,>=8.0.1; extra == "dev"
|
|
35
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
|
-
Requires-Dist: mkdocs-material<10.0.0,>=9.7.
|
|
39
|
-
Requires-Dist: pudb<2026.0.0,>=2025.1.
|
|
40
|
-
Requires-Dist: pylint<5.0.0,>=4.0.
|
|
38
|
+
Requires-Dist: mkdocs-material<10.0.0,>=9.7.6; extra == "dev"
|
|
39
|
+
Requires-Dist: pudb<2026.0.0,>=2025.1.5; extra == "dev"
|
|
40
|
+
Requires-Dist: pylint<5.0.0,>=4.0.5; extra == "dev"
|
|
41
41
|
Requires-Dist: pyright<2.0.0,>=1.1.407; extra == "dev"
|
|
42
42
|
Requires-Dist: pytest<10.0.0,>=9.0.3; extra == "dev"
|
|
43
43
|
Requires-Dist: requests<3.0.0,>=2.33.1; extra == "dev"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This module defines the root command-line interface for PyPDFForm.
|
|
4
|
+
|
|
5
|
+
It creates the Typer application, attaches the `create`, `inspect`, and `update`
|
|
6
|
+
command groups, and exposes top-level options shared by those commands. The root
|
|
7
|
+
callback collects global flags in the Typer context so each subcommand can
|
|
8
|
+
initialize `PdfWrapper` with consistent settings.
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
- `fill`: Fill an existing PDF form from JSON data.
|
|
12
|
+
- `create`: Create PDFs, fields, annotations, raw elements, and grid views.
|
|
13
|
+
- `inspect`: Print form metadata and field data as JSON.
|
|
14
|
+
- `update`: Modify PDF metadata, field names, properties, geometry, and scripts.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Annotated
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
|
|
23
|
+
from .. import PdfWrapper, Widgets, __version__
|
|
24
|
+
from .common import INPUT_PDF, OPTIONAL_OUTPUT_PDF, json_file_option
|
|
25
|
+
from .create import create_cli
|
|
26
|
+
from .inspect import inspect_cli
|
|
27
|
+
from .update import update_cli
|
|
28
|
+
|
|
29
|
+
cli_app = typer.Typer(
|
|
30
|
+
context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
|
|
31
|
+
)
|
|
32
|
+
cli_app.add_typer(
|
|
33
|
+
create_cli,
|
|
34
|
+
name="create",
|
|
35
|
+
help="Create PDFs and PDF elements.",
|
|
36
|
+
)
|
|
37
|
+
cli_app.add_typer(
|
|
38
|
+
inspect_cli,
|
|
39
|
+
name="inspect",
|
|
40
|
+
help="Inspect PDF form information.",
|
|
41
|
+
)
|
|
42
|
+
cli_app.add_typer(
|
|
43
|
+
update_cli,
|
|
44
|
+
name="update",
|
|
45
|
+
help="Update PDF metadata, fields, and scripts.",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def version_callback(value: bool) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Handles the global version option.
|
|
52
|
+
|
|
53
|
+
This callback is invoked eagerly by Typer when `--version` or `-v` is
|
|
54
|
+
passed. When the option is enabled, it prints the current PyPDFForm version
|
|
55
|
+
and exits before command parsing continues.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
value (bool): Whether the version flag was supplied.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
typer.Exit: Raised after printing the version so the CLI exits without
|
|
62
|
+
running another command.
|
|
63
|
+
"""
|
|
64
|
+
if value:
|
|
65
|
+
typer.echo(f"v{__version__}")
|
|
66
|
+
raise typer.Exit
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@cli_app.callback(
|
|
70
|
+
invoke_without_command=True,
|
|
71
|
+
help="Create, fill, inspect, and update PDF forms.",
|
|
72
|
+
)
|
|
73
|
+
def main(
|
|
74
|
+
ctx: typer.Context,
|
|
75
|
+
version: Annotated[ # pylint: disable=W0613
|
|
76
|
+
bool,
|
|
77
|
+
typer.Option(
|
|
78
|
+
"--version",
|
|
79
|
+
"-v",
|
|
80
|
+
callback=version_callback,
|
|
81
|
+
is_eager=True,
|
|
82
|
+
help="Show the PyPDFForm version and exit.",
|
|
83
|
+
),
|
|
84
|
+
] = False,
|
|
85
|
+
need_appearances: Annotated[
|
|
86
|
+
bool,
|
|
87
|
+
typer.Option(
|
|
88
|
+
"--need-appearances",
|
|
89
|
+
help="Ask PDF viewers to render form field appearances.",
|
|
90
|
+
),
|
|
91
|
+
] = False,
|
|
92
|
+
generate_appearance_streams: Annotated[
|
|
93
|
+
bool,
|
|
94
|
+
typer.Option(
|
|
95
|
+
"--generate-appearance-streams",
|
|
96
|
+
help="Generate form field appearance streams.",
|
|
97
|
+
),
|
|
98
|
+
] = False,
|
|
99
|
+
preserve_metadata: Annotated[
|
|
100
|
+
bool,
|
|
101
|
+
typer.Option(
|
|
102
|
+
"--preserve-metadata",
|
|
103
|
+
help="Preserve input PDF metadata.",
|
|
104
|
+
),
|
|
105
|
+
] = False,
|
|
106
|
+
use_full_widget_name: Annotated[
|
|
107
|
+
bool,
|
|
108
|
+
typer.Option(
|
|
109
|
+
"--use-full-widget-name",
|
|
110
|
+
help="Use full form field names for lookup.",
|
|
111
|
+
),
|
|
112
|
+
] = False,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Create, fill, inspect, and update PDF forms."""
|
|
115
|
+
ctx.obj = {
|
|
116
|
+
"need_appearances": need_appearances,
|
|
117
|
+
"generate_appearance_streams": generate_appearance_streams,
|
|
118
|
+
"preserve_metadata": preserve_metadata,
|
|
119
|
+
"use_full_widget_name": use_full_widget_name,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@cli_app.command(no_args_is_help=True)
|
|
124
|
+
def fill(
|
|
125
|
+
ctx: typer.Context,
|
|
126
|
+
pdf: INPUT_PDF,
|
|
127
|
+
data: Annotated[Path, json_file_option("JSON file with form field values.")],
|
|
128
|
+
output: OPTIONAL_OUTPUT_PDF = None,
|
|
129
|
+
flatten: Annotated[
|
|
130
|
+
bool,
|
|
131
|
+
typer.Option("--flatten", help="Flatten form fields after filling."),
|
|
132
|
+
] = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Fill a PDF form with JSON data."""
|
|
135
|
+
with open(data, "r", encoding="utf-8") as f:
|
|
136
|
+
input_data = json.load(f)
|
|
137
|
+
|
|
138
|
+
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
139
|
+
for k, each in obj.widgets.items():
|
|
140
|
+
if k in input_data and isinstance(each, (Widgets.Image, Widgets.Signature)):
|
|
141
|
+
each.preserve_aspect_ratio = input_data.get(k, {}).get(
|
|
142
|
+
"preserve_aspect_ratio", each.preserve_aspect_ratio
|
|
143
|
+
)
|
|
144
|
+
input_data[k] = input_data[k]["path"]
|
|
145
|
+
|
|
146
|
+
obj.fill(input_data, flatten=flatten).write(output or pdf)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["cli_app"]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This module provides shared helpers for PyPDFForm CLI commands.
|
|
4
|
+
|
|
5
|
+
It contains utilities for loading JSON command input, registering custom fonts
|
|
6
|
+
once per command invocation, and converting grouped JSON element definitions
|
|
7
|
+
into the objects expected by `PdfWrapper` methods.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated, NoReturn
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
from .. import PdfWrapper
|
|
17
|
+
from ..lib.middleware.base import Widget
|
|
18
|
+
|
|
19
|
+
INPUT_PDF = Annotated[
|
|
20
|
+
Path,
|
|
21
|
+
typer.Argument(
|
|
22
|
+
exists=True,
|
|
23
|
+
file_okay=True,
|
|
24
|
+
dir_okay=False,
|
|
25
|
+
readable=True,
|
|
26
|
+
resolve_path=True,
|
|
27
|
+
help="Input PDF path.",
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
REQUIRED_OUTPUT_PDF = Annotated[
|
|
31
|
+
Path,
|
|
32
|
+
typer.Option(
|
|
33
|
+
"--output",
|
|
34
|
+
"-o",
|
|
35
|
+
file_okay=True,
|
|
36
|
+
dir_okay=False,
|
|
37
|
+
writable=True,
|
|
38
|
+
resolve_path=True,
|
|
39
|
+
help="Output PDF path.",
|
|
40
|
+
),
|
|
41
|
+
]
|
|
42
|
+
OPTIONAL_OUTPUT_PDF = Annotated[
|
|
43
|
+
Path | None,
|
|
44
|
+
typer.Option(
|
|
45
|
+
"--output",
|
|
46
|
+
"-o",
|
|
47
|
+
file_okay=True,
|
|
48
|
+
dir_okay=False,
|
|
49
|
+
writable=True,
|
|
50
|
+
resolve_path=True,
|
|
51
|
+
help="Output PDF path. Overwrites the input when omitted.",
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
FIELD_NAME = Annotated[str, typer.Option("--field", help="Form field name.")]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def json_file_option(help_text: str):
|
|
58
|
+
"""
|
|
59
|
+
Creates the common validated JSON file option.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
help_text (str): Help text to display for the option.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
typer.Option: A configured `--file` / `-f` option for JSON file input.
|
|
66
|
+
"""
|
|
67
|
+
return typer.Option(
|
|
68
|
+
"--file",
|
|
69
|
+
"-f",
|
|
70
|
+
exists=True,
|
|
71
|
+
file_okay=True,
|
|
72
|
+
dir_okay=False,
|
|
73
|
+
readable=True,
|
|
74
|
+
resolve_path=True,
|
|
75
|
+
help=help_text,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def cli_bad_parameter(
|
|
80
|
+
message: str,
|
|
81
|
+
param_hint: str,
|
|
82
|
+
cause: BaseException,
|
|
83
|
+
) -> NoReturn:
|
|
84
|
+
"""
|
|
85
|
+
Raises a Typer input error with a stable CLI message.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
message (str): Error message to display to the CLI user.
|
|
89
|
+
param_hint (str): CLI parameter associated with the error.
|
|
90
|
+
cause (BaseException): Original exception that caused the CLI error.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
typer.BadParameter: Raised with the provided message and parameter hint.
|
|
94
|
+
"""
|
|
95
|
+
raise typer.BadParameter(message, param_hint=param_hint) from cause
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
|
|
99
|
+
"""
|
|
100
|
+
Look up a widget and report missing names as CLI input errors.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
wrapper (PdfWrapper): PDF wrapper containing form widgets.
|
|
104
|
+
field (str): Form field name to look up.
|
|
105
|
+
param_hint (str): CLI parameter associated with the field name.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Widget: The matching widget.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
typer.BadParameter: Raised when the widget name is not present.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
return wrapper.widgets[field]
|
|
115
|
+
except KeyError as exc:
|
|
116
|
+
cli_bad_parameter(
|
|
117
|
+
f"Form field '{field}' does not exist.",
|
|
118
|
+
param_hint=param_hint,
|
|
119
|
+
cause=exc,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def handle_font_registration(
|
|
124
|
+
obj: PdfWrapper, params: dict, registered_font: dict
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Registers a custom font referenced by CLI input.
|
|
128
|
+
|
|
129
|
+
CLI JSON files may provide a file path in a `font` parameter. This helper
|
|
130
|
+
registers each unique font path on the supplied `PdfWrapper` once, assigns
|
|
131
|
+
it a generated internal font name, and mutates `params["font"]` to that
|
|
132
|
+
registered name so downstream field or element constructors can use it.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
obj (PdfWrapper): The wrapper for the PDF currently being modified.
|
|
136
|
+
params (dict): The element or widget parameters loaded from JSON. This
|
|
137
|
+
dictionary is mutated when it contains a `font` key.
|
|
138
|
+
registered_font (dict): Mapping of source font paths to generated
|
|
139
|
+
`PdfWrapper` font names for the current command invocation.
|
|
140
|
+
"""
|
|
141
|
+
if "font" in params:
|
|
142
|
+
if params["font"] not in registered_font:
|
|
143
|
+
font_name = f"new_font_{len(registered_font)}"
|
|
144
|
+
obj.register_font(font_name, params["font"])
|
|
145
|
+
registered_font[params["font"]] = font_name
|
|
146
|
+
params["font"] = registered_font[params["font"]]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def create_elements_from_file(
|
|
150
|
+
pdf: Path,
|
|
151
|
+
data: Path,
|
|
152
|
+
element_map: dict,
|
|
153
|
+
method_name: str,
|
|
154
|
+
ctx: typer.Context,
|
|
155
|
+
output: Path | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Creates PDF elements from grouped JSON definitions.
|
|
159
|
+
|
|
160
|
+
The input JSON is expected to group element definitions by type, such as
|
|
161
|
+
`text`, `image`, or `highlight`. Each group key is resolved through
|
|
162
|
+
`element_map`, each item is constructed after optional font registration,
|
|
163
|
+
and the resulting objects are passed to `method_name` on `PdfWrapper`.
|
|
164
|
+
The modified PDF is written to `output` or back to the input path.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
pdf (Path): The path to the input PDF file.
|
|
168
|
+
data (Path): The path to the JSON file containing grouped element
|
|
169
|
+
definitions.
|
|
170
|
+
element_map (dict): Mapping from JSON group names to element classes or
|
|
171
|
+
callables used to construct each object.
|
|
172
|
+
method_name (str): Name of the `PdfWrapper` method that accepts the
|
|
173
|
+
constructed elements, such as `bulk_create_fields`, `draw`, or
|
|
174
|
+
`annotate`.
|
|
175
|
+
ctx (typer.Context): Typer context containing global wrapper options in
|
|
176
|
+
`ctx.obj`.
|
|
177
|
+
output (Path, optional): Path where the modified PDF should be saved. If
|
|
178
|
+
omitted, the input PDF is overwritten. Defaults to None.
|
|
179
|
+
"""
|
|
180
|
+
with open(data, "r", encoding="utf-8") as f:
|
|
181
|
+
input_data = json.load(f)
|
|
182
|
+
|
|
183
|
+
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
184
|
+
ungrouped_input = []
|
|
185
|
+
registered_font = {}
|
|
186
|
+
for k, v in input_data.items():
|
|
187
|
+
for each in v:
|
|
188
|
+
handle_font_registration(obj, each, registered_font)
|
|
189
|
+
ungrouped_input.append(element_map[k](**each))
|
|
190
|
+
|
|
191
|
+
getattr(obj, method_name)(ungrouped_input).write(output or pdf)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This module defines CLI commands for creating PDF files and PDF content.
|
|
4
|
+
|
|
5
|
+
It exposes the `create` command group for blank PDFs, extracted page ranges,
|
|
6
|
+
merged PDFs, form fields, raw drawn elements, annotations, and coordinate grid
|
|
7
|
+
views. Commands in this module translate command-line arguments or grouped JSON
|
|
8
|
+
input into `PdfWrapper`, `BlankPage`, `Fields`, `RawElements`, and
|
|
9
|
+
`Annotations` operations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from .. import (Annotations, BlankPage, Fields, PdfArray, PdfWrapper,
|
|
18
|
+
RawElements)
|
|
19
|
+
from .common import (INPUT_PDF, OPTIONAL_OUTPUT_PDF, REQUIRED_OUTPUT_PDF,
|
|
20
|
+
create_elements_from_file, json_file_option)
|
|
21
|
+
|
|
22
|
+
create_cli = typer.Typer(
|
|
23
|
+
context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@create_cli.command(no_args_is_help=True)
|
|
28
|
+
def blank(
|
|
29
|
+
ctx: typer.Context,
|
|
30
|
+
output: REQUIRED_OUTPUT_PDF,
|
|
31
|
+
count: Annotated[
|
|
32
|
+
int,
|
|
33
|
+
typer.Option(
|
|
34
|
+
"--count",
|
|
35
|
+
"-c",
|
|
36
|
+
min=1,
|
|
37
|
+
help="Number of blank pages to create.",
|
|
38
|
+
),
|
|
39
|
+
] = None,
|
|
40
|
+
width: Annotated[
|
|
41
|
+
float,
|
|
42
|
+
typer.Option(
|
|
43
|
+
"--width",
|
|
44
|
+
min=0.0,
|
|
45
|
+
help="Page width in points.",
|
|
46
|
+
),
|
|
47
|
+
] = None,
|
|
48
|
+
height: Annotated[
|
|
49
|
+
float,
|
|
50
|
+
typer.Option(
|
|
51
|
+
"--height",
|
|
52
|
+
min=0.0,
|
|
53
|
+
help="Page height in points.",
|
|
54
|
+
),
|
|
55
|
+
] = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Create a new blank PDF."""
|
|
58
|
+
params = {}
|
|
59
|
+
if width is not None:
|
|
60
|
+
params["width"] = width
|
|
61
|
+
if height is not None:
|
|
62
|
+
params["height"] = height
|
|
63
|
+
|
|
64
|
+
obj = BlankPage(**params)
|
|
65
|
+
if count is not None and count > 1:
|
|
66
|
+
obj = BlankPage(**params) * count
|
|
67
|
+
|
|
68
|
+
PdfWrapper(obj, **ctx.obj).write(output)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@create_cli.command(no_args_is_help=True)
|
|
72
|
+
def extract(
|
|
73
|
+
ctx: typer.Context,
|
|
74
|
+
pdf: INPUT_PDF,
|
|
75
|
+
output: REQUIRED_OUTPUT_PDF,
|
|
76
|
+
start: Annotated[
|
|
77
|
+
int,
|
|
78
|
+
typer.Option(
|
|
79
|
+
"--start",
|
|
80
|
+
"-s",
|
|
81
|
+
min=1,
|
|
82
|
+
help="First page to extract, starting at 1.",
|
|
83
|
+
),
|
|
84
|
+
] = None,
|
|
85
|
+
end: Annotated[
|
|
86
|
+
int,
|
|
87
|
+
typer.Option(
|
|
88
|
+
"--end",
|
|
89
|
+
"-e",
|
|
90
|
+
min=1,
|
|
91
|
+
help="Last page to extract, starting at 1.",
|
|
92
|
+
),
|
|
93
|
+
] = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Extract pages from an existing PDF."""
|
|
96
|
+
PdfWrapper(str(pdf), **ctx.obj).pages[slice((start or 1) - 1, end)].write(output)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@create_cli.command(no_args_is_help=True)
|
|
100
|
+
def merge(
|
|
101
|
+
ctx: typer.Context,
|
|
102
|
+
pdfs: Annotated[
|
|
103
|
+
list[Path],
|
|
104
|
+
typer.Argument(
|
|
105
|
+
exists=True,
|
|
106
|
+
file_okay=True,
|
|
107
|
+
dir_okay=False,
|
|
108
|
+
readable=True,
|
|
109
|
+
resolve_path=True,
|
|
110
|
+
help="Input PDF paths in merge order.",
|
|
111
|
+
),
|
|
112
|
+
],
|
|
113
|
+
output: REQUIRED_OUTPUT_PDF,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Merge multiple PDFs into one."""
|
|
116
|
+
PdfArray([PdfWrapper(str(pdf), **ctx.obj) for pdf in pdfs]).merge().write(output)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@create_cli.command(no_args_is_help=True)
|
|
120
|
+
def field(
|
|
121
|
+
ctx: typer.Context,
|
|
122
|
+
pdf: INPUT_PDF,
|
|
123
|
+
data: Annotated[Path, json_file_option("JSON file with form field definitions.")],
|
|
124
|
+
output: OPTIONAL_OUTPUT_PDF = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Add form fields to a PDF."""
|
|
127
|
+
field_map = {
|
|
128
|
+
"text": Fields.TextField,
|
|
129
|
+
"check": Fields.CheckBoxField,
|
|
130
|
+
"radio": Fields.RadioGroup,
|
|
131
|
+
"dropdown": Fields.DropdownField,
|
|
132
|
+
"image": Fields.ImageField,
|
|
133
|
+
"signature": Fields.SignatureField,
|
|
134
|
+
}
|
|
135
|
+
create_elements_from_file(pdf, data, field_map, "bulk_create_fields", ctx, output)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@create_cli.command(no_args_is_help=True)
|
|
139
|
+
def raw(
|
|
140
|
+
ctx: typer.Context,
|
|
141
|
+
pdf: INPUT_PDF,
|
|
142
|
+
data: Annotated[Path, json_file_option("JSON file with raw element definitions.")],
|
|
143
|
+
output: OPTIONAL_OUTPUT_PDF = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Draw text, images, and shapes on a PDF."""
|
|
146
|
+
raw_element_map = {
|
|
147
|
+
"text": RawElements.RawText,
|
|
148
|
+
"image": RawElements.RawImage,
|
|
149
|
+
"line": RawElements.RawLine,
|
|
150
|
+
"rectangle": RawElements.RawRectangle,
|
|
151
|
+
"circle": RawElements.RawCircle,
|
|
152
|
+
"ellipse": RawElements.RawEllipse,
|
|
153
|
+
}
|
|
154
|
+
create_elements_from_file(pdf, data, raw_element_map, "draw", ctx, output)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@create_cli.command(no_args_is_help=True)
|
|
158
|
+
def annotation(
|
|
159
|
+
ctx: typer.Context,
|
|
160
|
+
pdf: INPUT_PDF,
|
|
161
|
+
data: Annotated[Path, json_file_option("JSON file with annotation definitions.")],
|
|
162
|
+
output: OPTIONAL_OUTPUT_PDF = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Add annotations to a PDF."""
|
|
165
|
+
annotation_map = {
|
|
166
|
+
"text": Annotations.TextAnnotation,
|
|
167
|
+
"link": Annotations.LinkAnnotation,
|
|
168
|
+
"highlight": Annotations.HighlightAnnotation,
|
|
169
|
+
"underline": Annotations.UnderlineAnnotation,
|
|
170
|
+
"squiggly": Annotations.SquigglyAnnotation,
|
|
171
|
+
"strikeout": Annotations.StrikeOutAnnotation,
|
|
172
|
+
"stamp": Annotations.RubberStampAnnotation,
|
|
173
|
+
}
|
|
174
|
+
create_elements_from_file(pdf, data, annotation_map, "annotate", ctx, output)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@create_cli.command(no_args_is_help=True)
|
|
178
|
+
def grid(
|
|
179
|
+
ctx: typer.Context,
|
|
180
|
+
pdf: INPUT_PDF,
|
|
181
|
+
output: OPTIONAL_OUTPUT_PDF = None,
|
|
182
|
+
red: Annotated[
|
|
183
|
+
float,
|
|
184
|
+
typer.Option(
|
|
185
|
+
"--red",
|
|
186
|
+
"-r",
|
|
187
|
+
min=0.0,
|
|
188
|
+
max=1.0,
|
|
189
|
+
help="Grid red value, from 0 to 1.",
|
|
190
|
+
),
|
|
191
|
+
] = None,
|
|
192
|
+
green: Annotated[
|
|
193
|
+
float,
|
|
194
|
+
typer.Option(
|
|
195
|
+
"--green",
|
|
196
|
+
"-g",
|
|
197
|
+
min=0.0,
|
|
198
|
+
max=1.0,
|
|
199
|
+
help="Grid green value, from 0 to 1.",
|
|
200
|
+
),
|
|
201
|
+
] = None,
|
|
202
|
+
blue: Annotated[
|
|
203
|
+
float,
|
|
204
|
+
typer.Option(
|
|
205
|
+
"--blue",
|
|
206
|
+
"-b",
|
|
207
|
+
min=0.0,
|
|
208
|
+
max=1.0,
|
|
209
|
+
help="Grid blue value, from 0 to 1.",
|
|
210
|
+
),
|
|
211
|
+
] = None,
|
|
212
|
+
margin: Annotated[
|
|
213
|
+
float,
|
|
214
|
+
typer.Option(
|
|
215
|
+
"--margin",
|
|
216
|
+
"-m",
|
|
217
|
+
min=0.0,
|
|
218
|
+
help="Grid margin in points.",
|
|
219
|
+
),
|
|
220
|
+
] = None,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Add a coordinate grid to a PDF."""
|
|
223
|
+
params = {}
|
|
224
|
+
if any(
|
|
225
|
+
[
|
|
226
|
+
red is not None,
|
|
227
|
+
green is not None,
|
|
228
|
+
blue is not None,
|
|
229
|
+
]
|
|
230
|
+
):
|
|
231
|
+
params["color"] = (red or 0, green or 0, blue or 0)
|
|
232
|
+
|
|
233
|
+
if margin is not None:
|
|
234
|
+
params["margin"] = int(margin) if margin.is_integer() else margin
|
|
235
|
+
PdfWrapper(str(pdf), **ctx.obj).generate_coordinate_grid(**params).write(
|
|
236
|
+
output or pdf
|
|
237
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This module defines CLI commands for inspecting PDF form information.
|
|
4
|
+
|
|
5
|
+
It exposes the `inspect` command group, which prints JSON for form schemas,
|
|
6
|
+
current form values, generated sample data, and field rectangle metadata.
|
|
7
|
+
Each command wraps read-only `PdfWrapper` properties so users can inspect forms
|
|
8
|
+
from the terminal without writing Python code.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from .. import PdfWrapper
|
|
16
|
+
from .common import FIELD_NAME, INPUT_PDF, get_widget
|
|
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: INPUT_PDF,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Print the form schema as JSON."""
|
|
29
|
+
typer.echo(json.dumps(PdfWrapper(str(pdf), **ctx.obj).schema))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@inspect_cli.command(no_args_is_help=True)
|
|
33
|
+
def data(
|
|
34
|
+
ctx: typer.Context,
|
|
35
|
+
pdf: INPUT_PDF,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Print current form data as JSON."""
|
|
38
|
+
typer.echo(json.dumps(PdfWrapper(str(pdf), **ctx.obj).data))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@inspect_cli.command(no_args_is_help=True)
|
|
42
|
+
def sample(
|
|
43
|
+
ctx: typer.Context,
|
|
44
|
+
pdf: INPUT_PDF,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Print sample fill data as JSON."""
|
|
47
|
+
typer.echo(json.dumps(PdfWrapper(str(pdf), **ctx.obj).sample_data))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@inspect_cli.command(no_args_is_help=True)
|
|
51
|
+
def location(
|
|
52
|
+
ctx: typer.Context,
|
|
53
|
+
pdf: INPUT_PDF,
|
|
54
|
+
field: FIELD_NAME,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Print a form field's location and size as JSON."""
|
|
57
|
+
f = get_widget(PdfWrapper(str(pdf), **ctx.obj), field, "--field")
|
|
58
|
+
|
|
59
|
+
typer.echo(
|
|
60
|
+
json.dumps(
|
|
61
|
+
{
|
|
62
|
+
"page_number": f.page_number,
|
|
63
|
+
"x": f.x,
|
|
64
|
+
"y": f.y,
|
|
65
|
+
"width": f.width,
|
|
66
|
+
"height": f.height,
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
)
|