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.
Files changed (89) hide show
  1. {pypdfform-4.8.0 → pypdfform-4.8.2}/PKG-INFO +7 -7
  2. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/__init__.py +1 -1
  3. pypdfform-4.8.2/PyPDFForm/cli/__init__.py +170 -0
  4. pypdfform-4.8.2/PyPDFForm/cli/common.py +248 -0
  5. pypdfform-4.8.2/PyPDFForm/cli/create.py +265 -0
  6. pypdfform-4.8.2/PyPDFForm/cli/inspect.py +69 -0
  7. pypdfform-4.8.2/PyPDFForm/cli/schemas/create.py +525 -0
  8. pypdfform-4.8.2/PyPDFForm/cli/schemas/update.py +74 -0
  9. pypdfform-4.8.2/PyPDFForm/cli/update.py +194 -0
  10. pypdfform-4.8.2/PyPDFForm/lib/assets/__init__.py +0 -0
  11. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/constants.py +21 -10
  12. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/hooks.py +1 -1
  13. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/dropdown.py +8 -3
  14. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/wrapper.py +1 -0
  15. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/PKG-INFO +7 -7
  16. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/SOURCES.txt +4 -2
  17. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/requires.txt +6 -6
  18. {pypdfform-4.8.0 → pypdfform-4.8.2}/pyproject.toml +6 -7
  19. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_dropdown.py +3 -1
  20. pypdfform-4.8.0/PyPDFForm/cli/__init__.py +0 -229
  21. pypdfform-4.8.0/PyPDFForm/cli/coordinate.py +0 -165
  22. pypdfform-4.8.0/PyPDFForm/cli/create.py +0 -169
  23. pypdfform-4.8.0/PyPDFForm/cli/inspect.py +0 -53
  24. pypdfform-4.8.0/PyPDFForm/cli/update.py +0 -45
  25. pypdfform-4.8.0/tests/test_cli.py +0 -71
  26. {pypdfform-4.8.0 → pypdfform-4.8.2}/LICENSE +0 -0
  27. {pypdfform-4.8.0/PyPDFForm/lib → pypdfform-4.8.2/PyPDFForm/cli/schemas}/__init__.py +0 -0
  28. {pypdfform-4.8.0/PyPDFForm/lib/assets → pypdfform-4.8.2/PyPDFForm/lib}/__init__.py +0 -0
  29. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/adapter.py +0 -0
  30. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/__init__.py +0 -0
  31. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/base.py +0 -0
  32. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/link.py +0 -0
  33. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/stamp.py +0 -0
  34. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/text.py +0 -0
  35. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  36. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/assets/bedrock.py +0 -0
  37. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/assets/blank.py +0 -0
  38. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/coordinate.py +0 -0
  39. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/deprecation.py +0 -0
  40. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/egress.py +0 -0
  41. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/filler.py +0 -0
  42. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/font.py +0 -0
  43. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/image.py +0 -0
  44. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/__init__.py +0 -0
  45. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/base.py +0 -0
  46. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  47. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/image.py +0 -0
  48. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/radio.py +0 -0
  49. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/signature.py +0 -0
  50. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/text.py +0 -0
  51. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/patterns.py +0 -0
  52. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/__init__.py +0 -0
  53. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/circle.py +0 -0
  54. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/ellipse.py +0 -0
  55. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/image.py +0 -0
  56. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/line.py +0 -0
  57. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/rect.py +0 -0
  58. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/raw/text.py +0 -0
  59. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/template.py +0 -0
  60. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/types.py +0 -0
  61. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/utils.py +0 -0
  62. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/watermark.py +0 -0
  63. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/__init__.py +0 -0
  64. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/base.py +0 -0
  65. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  66. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  67. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/image.py +0 -0
  68. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/radio.py +0 -0
  69. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/signature.py +0 -0
  70. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/text.py +0 -0
  71. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  72. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/entry_points.txt +0 -0
  73. {pypdfform-4.8.0 → pypdfform-4.8.2}/PyPDFForm.egg-info/top_level.txt +0 -0
  74. {pypdfform-4.8.0 → pypdfform-4.8.2}/README.md +0 -0
  75. {pypdfform-4.8.0 → pypdfform-4.8.2}/setup.cfg +0 -0
  76. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_bulk_create_fields.py +0 -0
  77. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_create_widget.py +0 -0
  78. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_draw_elements.py +0 -0
  79. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_extract_middleware_attributes.py +0 -0
  80. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_fill_max_length_text_field.py +0 -0
  81. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_font_widths.py +0 -0
  82. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_functional.py +0 -0
  83. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_generate_appearance_streams.py +0 -0
  84. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_js.py +0 -0
  85. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_need_appearances.py +0 -0
  86. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_paragraph.py +0 -0
  87. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_signature.py +0 -0
  88. {pypdfform-4.8.0 → pypdfform-4.8.2}/tests/test_use_full_widget_name.py +0 -0
  89. {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.0
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.0.0
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,>=25.11.0; extra == "dev"
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.0; extra == "dev"
39
- Requires-Dist: pudb<2026.0.0,>=2025.1.3; extra == "dev"
40
- Requires-Dist: pylint<5.0.0,>=4.0.3; extra == "dev"
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"
@@ -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.8.0"
25
+ __version__ = "4.8.2"
26
26
 
27
27
  from .lib.annotations import Annotations
28
28
  from .lib.assets.blank import BlankPage
@@ -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)