PyPDFForm 4.8.0__tar.gz → 4.8.2__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.2}/PKG-INFO +7 -7
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/__init__.py +1 -1
- pypdfform-4.8.2/PyPDFForm/cli/__init__.py +170 -0
- pypdfform-4.8.2/PyPDFForm/cli/common.py +248 -0
- pypdfform-4.8.2/PyPDFForm/cli/create.py +265 -0
- pypdfform-4.8.2/PyPDFForm/cli/inspect.py +69 -0
- pypdfform-4.8.2/PyPDFForm/cli/schemas/create.py +525 -0
- pypdfform-4.8.2/PyPDFForm/cli/schemas/update.py +74 -0
- pypdfform-4.8.2/PyPDFForm/cli/update.py +194 -0
- pypdfform-4.8.2/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/constants.py +21 -10
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/hooks.py +1 -1
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/dropdown.py +8 -3
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/wrapper.py +1 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/PKG-INFO +7 -7
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/SOURCES.txt +4 -2
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/requires.txt +6 -6
- {pypdfform-4.8.0 → pypdfform-4.8.2}/pyproject.toml +6 -7
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_dropdown.py +3 -1
- 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.2}/LICENSE +0 -0
- {pypdfform-4.8.0/PyPDFForm/lib → pypdfform-4.8.2/PyPDFForm/cli/schemas}/__init__.py +0 -0
- {pypdfform-4.8.0/PyPDFForm/lib/assets → pypdfform-4.8.2/PyPDFForm/lib}/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/egress.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/font.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/patterns.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/template.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/watermark.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/base.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/signature.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/README.md +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/setup.cfg +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_create_widget.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_draw_elements.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_font_widths.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_functional.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_js.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_need_appearances.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_paragraph.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_signature.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_use_full_widget_name.py +0 -0
- {pypdfform-4.8.0 → pypdfform-4.8.2}/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.2
|
|
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
|
+
Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "cli"
|
|
31
32
|
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: black<27.0.0,>=
|
|
33
|
+
Requires-Dist: black<27.0.0,>=26.3.1; extra == "dev"
|
|
33
34
|
Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
|
|
34
35
|
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
|
-
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,170 @@
|
|
|
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
|
+
from pathlib import Path
|
|
18
|
+
from typing import Annotated
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
from .. import PdfWrapper, Widgets, __version__
|
|
23
|
+
from .common import (INPUT_PDF, OPTIONAL_OUTPUT_PDF, json_file_option,
|
|
24
|
+
load_json_file)
|
|
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
|
+
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
136
|
+
|
|
137
|
+
schema = obj.schema
|
|
138
|
+
for key, widget in obj.widgets.items():
|
|
139
|
+
if isinstance(widget, (Widgets.Image, Widgets.Signature)):
|
|
140
|
+
schema["properties"][key] = {
|
|
141
|
+
"anyOf": [
|
|
142
|
+
schema["properties"][key],
|
|
143
|
+
{
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {
|
|
146
|
+
"path": {"type": "string"},
|
|
147
|
+
"preserve_aspect_ratio": {"type": "boolean"},
|
|
148
|
+
},
|
|
149
|
+
"required": ["path"],
|
|
150
|
+
"additionalProperties": False,
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
input_data = load_json_file(data, schema, "--file")
|
|
156
|
+
for k, each in obj.widgets.items():
|
|
157
|
+
if (
|
|
158
|
+
k in input_data
|
|
159
|
+
and isinstance(each, (Widgets.Image, Widgets.Signature))
|
|
160
|
+
and isinstance(input_data[k], dict)
|
|
161
|
+
):
|
|
162
|
+
each.preserve_aspect_ratio = input_data[k].get(
|
|
163
|
+
"preserve_aspect_ratio", each.preserve_aspect_ratio
|
|
164
|
+
)
|
|
165
|
+
input_data[k] = input_data[k]["path"]
|
|
166
|
+
|
|
167
|
+
obj.fill(input_data, flatten=flatten).write(output or pdf)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
__all__ = ["cli_app"]
|
|
@@ -0,0 +1,248 @@
|
|
|
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, Any, NoReturn
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from jsonschema import ValidationError, validate
|
|
16
|
+
|
|
17
|
+
from .. import PdfWrapper
|
|
18
|
+
from ..lib.middleware.base import Widget
|
|
19
|
+
|
|
20
|
+
INPUT_PDF = Annotated[
|
|
21
|
+
Path,
|
|
22
|
+
typer.Argument(
|
|
23
|
+
exists=True,
|
|
24
|
+
file_okay=True,
|
|
25
|
+
dir_okay=False,
|
|
26
|
+
readable=True,
|
|
27
|
+
resolve_path=True,
|
|
28
|
+
help="Input PDF path.",
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
REQUIRED_OUTPUT_PDF = Annotated[
|
|
32
|
+
Path,
|
|
33
|
+
typer.Option(
|
|
34
|
+
"--output",
|
|
35
|
+
"-o",
|
|
36
|
+
file_okay=True,
|
|
37
|
+
dir_okay=False,
|
|
38
|
+
writable=True,
|
|
39
|
+
resolve_path=True,
|
|
40
|
+
help="Output PDF path.",
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
OPTIONAL_OUTPUT_PDF = Annotated[
|
|
44
|
+
Path | None,
|
|
45
|
+
typer.Option(
|
|
46
|
+
"--output",
|
|
47
|
+
"-o",
|
|
48
|
+
file_okay=True,
|
|
49
|
+
dir_okay=False,
|
|
50
|
+
writable=True,
|
|
51
|
+
resolve_path=True,
|
|
52
|
+
help="Output PDF path. Overwrites the input when omitted.",
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
FIELD_NAME = Annotated[str, typer.Option("--field", help="Form field name.")]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def json_file_option(help_text: str):
|
|
59
|
+
"""
|
|
60
|
+
Creates the common validated JSON file option.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
help_text (str): Help text to display for the option.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
typer.Option: A configured `--file` / `-f` option for JSON file input.
|
|
67
|
+
"""
|
|
68
|
+
return typer.Option(
|
|
69
|
+
"--file",
|
|
70
|
+
"-f",
|
|
71
|
+
exists=True,
|
|
72
|
+
file_okay=True,
|
|
73
|
+
dir_okay=False,
|
|
74
|
+
readable=True,
|
|
75
|
+
resolve_path=True,
|
|
76
|
+
help=help_text,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cli_bad_parameter(
|
|
81
|
+
message: str,
|
|
82
|
+
param_hint: str,
|
|
83
|
+
cause: BaseException,
|
|
84
|
+
) -> NoReturn:
|
|
85
|
+
"""
|
|
86
|
+
Raises a Typer input error with a stable CLI message.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
message (str): Error message to display to the CLI user.
|
|
90
|
+
param_hint (str): CLI parameter associated with the error.
|
|
91
|
+
cause (BaseException): Original exception that caused the CLI error.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
typer.BadParameter: Raised with the provided message and parameter hint.
|
|
95
|
+
"""
|
|
96
|
+
raise typer.BadParameter(message, param_hint=param_hint) from cause
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _validation_error_path(exc: ValidationError) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Builds a dotted JSON path for a validation error.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
exc (ValidationError): The JSON schema validation error.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
str: Dotted path for the failing instance location.
|
|
108
|
+
"""
|
|
109
|
+
return ".".join(str(each) for each in exc.absolute_path)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_json_file(data: Path, schema: dict, param_hint: str) -> Any:
|
|
113
|
+
"""
|
|
114
|
+
Loads a JSON CLI input file and validates it against a schema.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
data (Path): JSON file path.
|
|
118
|
+
schema (dict): JSON schema to validate against.
|
|
119
|
+
param_hint (str): CLI parameter associated with the JSON file.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Any: Parsed and validated JSON input.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
typer.BadParameter: Raised when the file cannot be loaded or validation
|
|
126
|
+
fails.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
with open(data, "r", encoding="utf-8") as f:
|
|
130
|
+
input_data = json.load(f)
|
|
131
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
132
|
+
_cli_bad_parameter(
|
|
133
|
+
f"Invalid JSON file: {exc}",
|
|
134
|
+
param_hint=param_hint,
|
|
135
|
+
cause=exc,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
validate(instance=input_data, schema=schema)
|
|
140
|
+
except ValidationError as exc:
|
|
141
|
+
error_path = _validation_error_path(exc)
|
|
142
|
+
location = f" at {error_path}" if error_path else ""
|
|
143
|
+
_cli_bad_parameter(
|
|
144
|
+
f"Invalid JSON file{location}: {exc.message}",
|
|
145
|
+
param_hint=param_hint,
|
|
146
|
+
cause=exc,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return input_data
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
|
|
153
|
+
"""
|
|
154
|
+
Look up a widget and report missing names as CLI input errors.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
wrapper (PdfWrapper): PDF wrapper containing form widgets.
|
|
158
|
+
field (str): Form field name to look up.
|
|
159
|
+
param_hint (str): CLI parameter associated with the field name.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Widget: The matching widget.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
typer.BadParameter: Raised when the widget name is not present.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
return wrapper.widgets[field]
|
|
169
|
+
except KeyError as exc:
|
|
170
|
+
_cli_bad_parameter(
|
|
171
|
+
f"Form field '{field}' does not exist.",
|
|
172
|
+
param_hint=param_hint,
|
|
173
|
+
cause=exc,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def handle_font_registration(
|
|
178
|
+
obj: PdfWrapper, params: dict, registered_font: dict
|
|
179
|
+
) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Registers a custom font referenced by CLI input.
|
|
182
|
+
|
|
183
|
+
CLI JSON files may provide a file path in a `font` parameter. This helper
|
|
184
|
+
registers each unique font path on the supplied `PdfWrapper` once, assigns
|
|
185
|
+
it a generated internal font name, and mutates `params["font"]` to that
|
|
186
|
+
registered name so downstream field or element constructors can use it.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
obj (PdfWrapper): The wrapper for the PDF currently being modified.
|
|
190
|
+
params (dict): The element or widget parameters loaded from JSON. This
|
|
191
|
+
dictionary is mutated when it contains a `font` key.
|
|
192
|
+
registered_font (dict): Mapping of source font paths to generated
|
|
193
|
+
`PdfWrapper` font names for the current command invocation.
|
|
194
|
+
"""
|
|
195
|
+
if "font" in params:
|
|
196
|
+
if params["font"] not in registered_font:
|
|
197
|
+
font_name = f"new_font_{len(registered_font)}"
|
|
198
|
+
obj.register_font(font_name, params["font"])
|
|
199
|
+
registered_font[params["font"]] = font_name
|
|
200
|
+
params["font"] = registered_font[params["font"]]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_elements_from_file(
|
|
204
|
+
pdf: Path,
|
|
205
|
+
data: Path,
|
|
206
|
+
element_map: dict,
|
|
207
|
+
schema: dict,
|
|
208
|
+
method_name: str,
|
|
209
|
+
ctx: typer.Context,
|
|
210
|
+
param_hint: str,
|
|
211
|
+
output: Path | None = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Creates PDF elements from grouped JSON definitions.
|
|
215
|
+
|
|
216
|
+
The input JSON is expected to group element definitions by type, such as
|
|
217
|
+
`text`, `image`, or `highlight`. Each group key is resolved through
|
|
218
|
+
`element_map`, each item is constructed after optional font registration,
|
|
219
|
+
and the resulting objects are passed to `method_name` on `PdfWrapper`.
|
|
220
|
+
The modified PDF is written to `output` or back to the input path.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
pdf (Path): The path to the input PDF file.
|
|
224
|
+
data (Path): The path to the JSON file containing grouped element
|
|
225
|
+
definitions.
|
|
226
|
+
element_map (dict): Mapping from JSON group names to element classes or
|
|
227
|
+
callables used to construct each object.
|
|
228
|
+
schema (dict): JSON schema used to validate the grouped definitions.
|
|
229
|
+
method_name (str): Name of the `PdfWrapper` method that accepts the
|
|
230
|
+
constructed elements, such as `bulk_create_fields`, `draw`, or
|
|
231
|
+
`annotate`.
|
|
232
|
+
ctx (typer.Context): Typer context containing global wrapper options in
|
|
233
|
+
`ctx.obj`.
|
|
234
|
+
param_hint (str): CLI parameter associated with the JSON file.
|
|
235
|
+
output (Path, optional): Path where the modified PDF should be saved. If
|
|
236
|
+
omitted, the input PDF is overwritten. Defaults to None.
|
|
237
|
+
"""
|
|
238
|
+
input_data = load_json_file(data, schema, param_hint)
|
|
239
|
+
|
|
240
|
+
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
241
|
+
ungrouped_input = []
|
|
242
|
+
registered_font = {}
|
|
243
|
+
for k, v in input_data.items():
|
|
244
|
+
for each in v:
|
|
245
|
+
handle_font_registration(obj, each, registered_font)
|
|
246
|
+
ungrouped_input.append(element_map[k](**each))
|
|
247
|
+
|
|
248
|
+
getattr(obj, method_name)(ungrouped_input).write(output or pdf)
|