PyPDFForm 5.2.5__tar.gz → 5.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {pypdfform-5.2.5 → pypdfform-5.4.0}/PKG-INFO +2 -13
  2. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/__init__.py +1 -1
  3. pypdfform-5.4.0/PyPDFForm/cli/common.py +373 -0
  4. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/create.py +75 -30
  5. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/entry.py +1 -1
  6. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/root.py +25 -9
  7. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/update.py +97 -28
  8. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm.egg-info/PKG-INFO +2 -13
  9. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm.egg-info/requires.txt +1 -0
  10. {pypdfform-5.2.5 → pypdfform-5.4.0}/README.md +0 -12
  11. {pypdfform-5.2.5 → pypdfform-5.4.0}/pyproject.toml +5 -1
  12. pypdfform-5.2.5/PyPDFForm/cli/common.py +0 -259
  13. {pypdfform-5.2.5 → pypdfform-5.4.0}/LICENSE +0 -0
  14. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/__init__.py +0 -0
  15. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/inspect.py +0 -0
  16. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/remove.py +0 -0
  17. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/__init__.py +0 -0
  18. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/create.py +0 -0
  19. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/update.py +0 -0
  20. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/__init__.py +0 -0
  21. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/adapter.py +0 -0
  22. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/__init__.py +0 -0
  23. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/base.py +0 -0
  24. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/link.py +0 -0
  25. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/stamp.py +0 -0
  26. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/text.py +0 -0
  27. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  28. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/assets/__init__.py +0 -0
  29. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/assets/bedrock.py +0 -0
  30. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/assets/blank.py +0 -0
  31. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/constants.py +0 -0
  32. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/coordinate.py +0 -0
  33. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/deprecation.py +0 -0
  34. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/egress.py +0 -0
  35. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/filler.py +0 -0
  36. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/font.py +0 -0
  37. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/hooks.py +0 -0
  38. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/image.py +0 -0
  39. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/__init__.py +0 -0
  40. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/base.py +0 -0
  41. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  42. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  43. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/image.py +0 -0
  44. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/radio.py +0 -0
  45. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/signature.py +0 -0
  46. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/text.py +0 -0
  47. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/patterns.py +0 -0
  48. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/raw/__init__.py +0 -0
  49. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/raw/circle.py +0 -0
  50. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/raw/ellipse.py +0 -0
  51. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/raw/image.py +0 -0
  52. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/raw/line.py +0 -0
  53. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/raw/rect.py +0 -0
  54. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/raw/text.py +0 -0
  55. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/template.py +0 -0
  56. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/types.py +0 -0
  57. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/utils.py +0 -0
  58. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/watermark.py +0 -0
  59. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/__init__.py +0 -0
  60. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/base.py +0 -0
  61. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  62. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  63. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/image.py +0 -0
  64. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/radio.py +0 -0
  65. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/signature.py +0 -0
  66. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/text.py +0 -0
  67. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm/lib/wrapper.py +0 -0
  68. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  69. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  70. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm.egg-info/entry_points.txt +0 -0
  71. {pypdfform-5.2.5 → pypdfform-5.4.0}/PyPDFForm.egg-info/top_level.txt +0 -0
  72. {pypdfform-5.2.5 → pypdfform-5.4.0}/setup.cfg +0 -0
  73. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_bulk_create_fields.py +0 -0
  74. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_create_widget.py +0 -0
  75. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_draw_elements.py +0 -0
  76. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_dropdown.py +0 -0
  77. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_extract_middleware_attributes.py +0 -0
  78. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_fill_max_length_text_field.py +0 -0
  79. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_font_widths.py +0 -0
  80. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_functional.py +0 -0
  81. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_generate_appearance_streams.py +0 -0
  82. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_js.py +0 -0
  83. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_need_appearances.py +0 -0
  84. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_paragraph.py +0 -0
  85. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_signature.py +0 -0
  86. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_use_full_widget_name.py +0 -0
  87. {pypdfform-5.2.5 → pypdfform-5.4.0}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 5.2.5
3
+ Version: 5.4.0
4
4
  Summary: The Python library & CLI for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -33,6 +33,7 @@ Requires-Dist: reportlab<6.0.0,>=4.5.1
33
33
  Provides-Extra: cli
34
34
  Requires-Dist: typer<1.0.0,>=0.26.1; extra == "cli"
35
35
  Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "cli"
36
+ Requires-Dist: PyYAML<7.0.0,>=6.0.3; extra == "cli"
36
37
  Provides-Extra: dev
37
38
  Requires-Dist: coverage<8.0.0,>=7.14.1; extra == "dev"
38
39
  Requires-Dist: mike<3.0.0,>=2.2.0; extra == "dev"
@@ -120,15 +121,3 @@ The official documentation can be found on [the GitHub page](https://chinapandam
120
121
  <!-- TODO: remove WIP when finish recording -->
121
122
  * [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
122
123
  * [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
123
-
124
- ## Star History
125
-
126
- This project is maintained entirely in my spare time. If you like the project please consider starring the GitHub repository. It is the best way to keep me motivated and continue making the project better.
127
-
128
- <a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
129
- <picture>
130
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
131
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
132
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
133
- </picture>
134
- </a>
@@ -18,7 +18,7 @@ from Python code or from the command line.
18
18
 
19
19
  import logging
20
20
 
21
- __version__ = "5.2.5"
21
+ __version__ = "5.4.0"
22
22
 
23
23
  from .lib.annotations import Annotations
24
24
  from .lib.assets.blank import BlankPage
@@ -0,0 +1,373 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module provides shared helpers for PyPDFForm CLI commands.
4
+
5
+ It contains utilities for loading structured command input, registering custom
6
+ fonts once per command invocation, and converting grouped 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
+ import yaml
16
+ from jsonschema import ValidationError, validate
17
+
18
+ from .. import PdfWrapper
19
+ from ..lib.middleware.base import Widget
20
+
21
+ INPUT_PDF = Annotated[
22
+ Path,
23
+ typer.Argument(
24
+ exists=True,
25
+ file_okay=True,
26
+ dir_okay=False,
27
+ readable=True,
28
+ resolve_path=True,
29
+ help="Input PDF path.",
30
+ ),
31
+ ]
32
+ REQUIRED_OUTPUT_PDF = Annotated[
33
+ Path,
34
+ typer.Option(
35
+ "--output",
36
+ "-o",
37
+ file_okay=True,
38
+ dir_okay=False,
39
+ writable=True,
40
+ resolve_path=True,
41
+ help="Output PDF path.",
42
+ ),
43
+ ]
44
+ OPTIONAL_OUTPUT_PDF = Annotated[
45
+ Path | None,
46
+ typer.Option(
47
+ "--output",
48
+ "-o",
49
+ file_okay=True,
50
+ dir_okay=False,
51
+ writable=True,
52
+ resolve_path=True,
53
+ help="Output PDF path. Overwrites the input when omitted.",
54
+ ),
55
+ ]
56
+ FIELD_NAME = Annotated[str, typer.Option("--field", help="Form field name.")]
57
+ FIELD_NAMES = Annotated[
58
+ list[str],
59
+ typer.Option(
60
+ "--field",
61
+ help="Form field name. Repeat this option to select multiple fields.",
62
+ ),
63
+ ]
64
+
65
+
66
+ def data_file_option(help_text: str):
67
+ """
68
+ Creates the common validated structured data file option.
69
+
70
+ Args:
71
+ help_text (str): Help text to display for the option.
72
+
73
+ Returns:
74
+ typer.Option: A configured `--file` / `-f` option for file input.
75
+ """
76
+ return typer.Option(
77
+ "--file",
78
+ "-f",
79
+ exists=True,
80
+ file_okay=True,
81
+ dir_okay=False,
82
+ readable=True,
83
+ resolve_path=True,
84
+ help=help_text,
85
+ )
86
+
87
+
88
+ def cli_bad_parameter(
89
+ message: str,
90
+ param_hint: str,
91
+ cause: BaseException | None = None,
92
+ ) -> NoReturn:
93
+ """
94
+ Raises a Typer input error with a stable CLI message.
95
+
96
+ Args:
97
+ message (str): Error message to display to the CLI user.
98
+ param_hint (str): CLI parameter associated with the error.
99
+ cause (BaseException, optional): Original exception that caused the CLI
100
+ error. Defaults to None.
101
+
102
+ Raises:
103
+ typer.BadParameter: Raised with the provided message and parameter hint.
104
+ """
105
+ if cause is None:
106
+ raise typer.BadParameter(message, param_hint=param_hint)
107
+
108
+ raise typer.BadParameter(message, param_hint=param_hint) from cause
109
+
110
+
111
+ def _validation_error_path(exc: ValidationError) -> str:
112
+ """
113
+ Builds a dotted input data path for a validation error.
114
+
115
+ Args:
116
+ exc (ValidationError): The JSON schema validation error.
117
+
118
+ Returns:
119
+ str: Dotted path for the failing instance location.
120
+ """
121
+ return ".".join(str(each) for each in exc.absolute_path)
122
+
123
+
124
+ def _input_file_kind(data: Path) -> str:
125
+ """
126
+ Gets the structured data format to use for a CLI input file.
127
+
128
+ Args:
129
+ data (Path): Input file path.
130
+
131
+ Returns:
132
+ str: The upper-case format name for user-facing messages.
133
+ """
134
+ if data.suffix.lower() in {".yaml", ".yml"}:
135
+ return "YAML"
136
+
137
+ return "JSON"
138
+
139
+
140
+ def _validate_input_data(
141
+ input_data: Any,
142
+ schema: dict,
143
+ source: str,
144
+ param_hint: str,
145
+ ) -> Any:
146
+ """
147
+ Validates parsed CLI input against a JSON schema.
148
+
149
+ Args:
150
+ input_data (Any): Parsed input data to validate.
151
+ schema (dict): JSON schema to validate against.
152
+ source (str): User-facing description of the input source.
153
+ param_hint (str): CLI parameter associated with the input.
154
+
155
+ Returns:
156
+ Any: The validated input data.
157
+
158
+ Raises:
159
+ typer.BadParameter: Raised when validation fails.
160
+ """
161
+ try:
162
+ validate(instance=input_data, schema=schema)
163
+ except ValidationError as exc:
164
+ error_path = _validation_error_path(exc)
165
+ location = f" at {error_path}" if error_path else ""
166
+ cli_bad_parameter(
167
+ f"Invalid {source}{location}: {exc.message}",
168
+ param_hint=param_hint,
169
+ cause=exc,
170
+ )
171
+
172
+ return input_data
173
+
174
+
175
+ def load_data_file(data: Path, schema: dict, param_hint: str) -> Any:
176
+ """
177
+ Loads a YAML or JSON CLI input file and validates it against a schema.
178
+
179
+ Args:
180
+ data (Path): YAML or JSON file path.
181
+ schema (dict): JSON schema to validate against.
182
+ param_hint (str): CLI parameter associated with the input file.
183
+
184
+ Returns:
185
+ Any: Parsed and validated input data.
186
+
187
+ Raises:
188
+ typer.BadParameter: Raised when the file cannot be loaded or validation
189
+ fails.
190
+ """
191
+ input_kind = _input_file_kind(data)
192
+ try:
193
+ with open(data, "r", encoding="utf-8") as f:
194
+ input_data = yaml.safe_load(f) if input_kind == "YAML" else json.load(f)
195
+ except (OSError, json.JSONDecodeError, yaml.YAMLError) as exc:
196
+ cli_bad_parameter(
197
+ f"Invalid {input_kind} file: {exc}",
198
+ param_hint=param_hint,
199
+ cause=exc,
200
+ )
201
+
202
+ return _validate_input_data(
203
+ input_data,
204
+ schema,
205
+ f"{input_kind} file",
206
+ param_hint,
207
+ )
208
+
209
+
210
+ def load_data_options(options: list[str], schema: dict, param_hint: str) -> Any:
211
+ """
212
+ Parses command-line data options and validates them against a schema.
213
+
214
+ Options use ``--name value`` syntax. Each value is parsed with
215
+ :func:`yaml.safe_load`.
216
+
217
+ Args:
218
+ options (list[str]): Unrecognized command arguments captured by Typer.
219
+ schema (dict): JSON schema to validate the resulting option mapping
220
+ against.
221
+ param_hint (str): CLI parameter description used in error messages.
222
+
223
+ Returns:
224
+ Any: Parsed and validated option data.
225
+
226
+ Raises:
227
+ typer.BadParameter: Raised when the options cannot be parsed or the
228
+ resulting mapping fails validation.
229
+ """
230
+ if any(not option.startswith("--") for option in options[::2]):
231
+ cli_bad_parameter(
232
+ "Use '--name value' pairs with valid values.",
233
+ param_hint=param_hint,
234
+ )
235
+
236
+ try:
237
+ input_data = {
238
+ option[2:]: yaml.safe_load(value)
239
+ for option, value in zip(options[::2], options[1::2], strict=True)
240
+ }
241
+ except (ValueError, yaml.YAMLError) as exc:
242
+ cli_bad_parameter(
243
+ "Use '--name value' pairs with valid values.",
244
+ param_hint=param_hint,
245
+ cause=exc,
246
+ )
247
+
248
+ return _validate_input_data(input_data, schema, "CLI options", param_hint)
249
+
250
+
251
+ def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
252
+ """
253
+ Look up a widget and report missing names as CLI input errors.
254
+
255
+ Args:
256
+ wrapper (PdfWrapper): PDF wrapper containing form widgets.
257
+ field (str): Form field name to look up.
258
+ param_hint (str): CLI parameter associated with the field name.
259
+
260
+ Returns:
261
+ Widget: The matching widget.
262
+
263
+ Raises:
264
+ typer.BadParameter: Raised when the widget name is not present.
265
+ """
266
+ try:
267
+ return wrapper.widgets[field]
268
+ except KeyError as exc:
269
+ cli_bad_parameter(
270
+ f"Form field '{field}' does not exist.",
271
+ param_hint=param_hint,
272
+ cause=exc,
273
+ )
274
+
275
+
276
+ def handle_font_registration(
277
+ obj: PdfWrapper, params: dict, registered_font: dict
278
+ ) -> None:
279
+ """
280
+ Registers a custom font referenced by CLI input.
281
+
282
+ CLI input may provide a file path in a `font` parameter. This helper
283
+ registers each unique font path on the supplied `PdfWrapper` once, assigns
284
+ it a generated internal font name, and mutates `params["font"]` to that
285
+ registered name so downstream field or element constructors can use it.
286
+
287
+ Args:
288
+ obj (PdfWrapper): The wrapper for the PDF currently being modified.
289
+ params (dict): The element or widget parameters loaded from CLI input.
290
+ This dictionary is mutated when it contains a `font` key.
291
+ registered_font (dict): Mapping of resolved source font paths to
292
+ generated `PdfWrapper` font names for the current command
293
+ invocation.
294
+ """
295
+ if "font" in params:
296
+ font_path = str(Path(params["font"]).resolve())
297
+ if font_path not in registered_font:
298
+ font_name = f"new_font_{len(registered_font)}"
299
+ obj.register_font(font_name, font_path)
300
+ registered_font[font_path] = font_name
301
+ params["font"] = registered_font[font_path]
302
+
303
+
304
+ def create_elements_from_file(
305
+ pdf: Path,
306
+ data: Path | None,
307
+ element_map: dict,
308
+ schema: dict,
309
+ method_name: str,
310
+ ctx: typer.Context,
311
+ file_param_hint: str,
312
+ options_param_hint: str,
313
+ output: Path | None = None,
314
+ element_type: str | None = None,
315
+ ) -> None:
316
+ """
317
+ Creates PDF elements from a data file or command-line options.
318
+
319
+ A data file groups element definitions by type, such as `text`, `image`, or
320
+ `highlight`. Without a data file, one element of `element_type` is created
321
+ from the additional command-line options stored in ``ctx.args``. The data
322
+ file takes precedence when both input forms are supplied. Each definition
323
+ is constructed after optional font registration and passed to `method_name`
324
+ on `PdfWrapper`.
325
+
326
+ Args:
327
+ pdf (Path): The path to the input PDF file.
328
+ data (Path, optional): The path to the input file containing grouped
329
+ element definitions. Defaults to None.
330
+ element_map (dict): Mapping from input group names to element classes
331
+ or callables used to construct each object.
332
+ schema (dict): JSON schema used to validate file or command-line input.
333
+ method_name (str): Name of the `PdfWrapper` method that accepts the
334
+ constructed elements, such as `bulk_create_fields`, `draw`, or
335
+ `annotate`.
336
+ ctx (typer.Context): Typer context containing global wrapper options in
337
+ `ctx.obj`.
338
+ file_param_hint (str): CLI parameter associated with the input file.
339
+ options_param_hint (str): CLI parameter description associated with
340
+ command-line options.
341
+ output (Path, optional): Path where the modified PDF should be saved. If
342
+ omitted, the input PDF is overwritten. Defaults to None.
343
+ element_type (str, optional): Element type selected for command-line
344
+ input. Defaults to None.
345
+ """
346
+ if data is not None:
347
+ input_data = load_data_file(data, schema, file_param_hint)
348
+ else:
349
+ if element_type not in element_map:
350
+ choices = ", ".join(element_map)
351
+ cli_bad_parameter(
352
+ f"Choose one of: {choices}.",
353
+ param_hint="--type",
354
+ )
355
+ input_data = {
356
+ element_type: [
357
+ load_data_options(
358
+ ctx.args,
359
+ schema["properties"][element_type]["items"],
360
+ options_param_hint,
361
+ )
362
+ ]
363
+ }
364
+
365
+ obj = PdfWrapper(str(pdf), **ctx.obj)
366
+ ungrouped_input = []
367
+ registered_font = {}
368
+ for k, v in input_data.items():
369
+ for each in v:
370
+ handle_font_registration(obj, each, registered_font)
371
+ ungrouped_input.append(element_map[k](**each))
372
+
373
+ getattr(obj, method_name)(ungrouped_input).write(output or pdf)
@@ -4,8 +4,8 @@ This module defines CLI commands for creating PDF files and PDF content.
4
4
 
5
5
  It exposes the `create` command group for blank PDFs, extracted page ranges,
6
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
7
+ views. Commands in this module translate command-line arguments or grouped
8
+ YAML/JSON input into `PdfWrapper`, `BlankPage`, `Fields`, `RawElements`, and
9
9
  `Annotations` operations.
10
10
  """
11
11
 
@@ -21,7 +21,7 @@ from .common import (
21
21
  REQUIRED_OUTPUT_PDF,
22
22
  cli_bad_parameter,
23
23
  create_elements_from_file,
24
- json_file_option,
24
+ data_file_option,
25
25
  )
26
26
  from .schemas.create import ANNOTATION_SCHEMA, FIELD_SCHEMA, RAW_SCHEMA
27
27
 
@@ -188,30 +188,43 @@ def merge(
188
188
 
189
189
 
190
190
  @create_cli.command(
191
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
191
192
  no_args_is_help=True,
192
- help="Add form fields to a PDF.",
193
+ help="Add form fields from a YAML or JSON file or command-line options.",
193
194
  )
194
195
  def field(
195
196
  ctx: typer.Context,
196
197
  pdf: INPUT_PDF,
197
- data: Annotated[Path, json_file_option("JSON file with form field definitions.")],
198
+ data: Annotated[
199
+ Path | None, data_file_option("YAML or JSON file with form field definitions.")
200
+ ] = None,
198
201
  output: OPTIONAL_OUTPUT_PDF = None,
202
+ element_type: Annotated[
203
+ str | None,
204
+ typer.Option("--type", help="Type of form field to create without --file."),
205
+ ] = None,
199
206
  ) -> None:
200
207
  """
201
- Add form fields described by grouped JSON definitions.
208
+ Add form fields from a data file or command-line options.
202
209
 
203
- The command maps JSON groups such as `text`, `check`, and `signature` to
204
- PyPDFForm field classes, validates the input file against the CLI field
205
- schema, creates the corresponding field objects, and calls
206
- `PdfWrapper.bulk_create_fields` before writing the modified PDF.
210
+ The command maps input groups such as `text`, `check`, and `signature` to
211
+ PyPDFForm field classes, validates the input against the CLI field schema,
212
+ creates the corresponding field objects, and calls
213
+ `PdfWrapper.bulk_create_fields` before writing the modified PDF. A data file
214
+ can define multiple fields grouped by type. Without `--file`, `--type` and
215
+ the field options define one field. The data file takes precedence when
216
+ both input forms are supplied.
207
217
 
208
218
  Args:
209
219
  ctx (typer.Context): Typer context containing global `PdfWrapper`
210
220
  options in `ctx.obj`.
211
221
  pdf (Path): Input PDF path.
212
- data (Path): JSON file containing grouped form field definitions.
222
+ data (Path, optional): YAML or JSON file containing grouped form field
223
+ definitions. Defaults to None.
213
224
  output (Path, optional): Output PDF path. If omitted, the input PDF is
214
225
  overwritten. Defaults to None.
226
+ element_type (str, optional): Form field type used when `data` is
227
+ omitted. Defaults to None.
215
228
  """
216
229
  field_map = {
217
230
  "text": Fields.TextField,
@@ -228,36 +241,51 @@ def field(
228
241
  schema=FIELD_SCHEMA,
229
242
  method_name="bulk_create_fields",
230
243
  ctx=ctx,
231
- param_hint="--file",
244
+ file_param_hint="--file",
245
+ options_param_hint="field options",
232
246
  output=output,
247
+ element_type=element_type,
233
248
  )
234
249
 
235
250
 
236
251
  @create_cli.command(
252
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
237
253
  no_args_is_help=True,
238
- help="Draw text, images, and shapes on a PDF.",
254
+ help="Draw elements from a YAML or JSON file or command-line options.",
239
255
  )
240
256
  def raw(
241
257
  ctx: typer.Context,
242
258
  pdf: INPUT_PDF,
243
- data: Annotated[Path, json_file_option("JSON file with raw element definitions.")],
259
+ data: Annotated[
260
+ Path | None, data_file_option("YAML or JSON file with raw element definitions.")
261
+ ] = None,
244
262
  output: OPTIONAL_OUTPUT_PDF = None,
263
+ element_type: Annotated[
264
+ str | None,
265
+ typer.Option("--type", help="Type of element to draw without --file."),
266
+ ] = None,
245
267
  ) -> None:
246
268
  """
247
- Draw raw elements described by grouped JSON definitions.
269
+ Draw elements from a data file or command-line options.
248
270
 
249
- The command maps JSON groups such as `text`, `image`, and `rectangle` to
250
- raw element classes, validates the input file against the CLI raw element
251
- schema, creates the corresponding drawable objects, and calls
252
- `PdfWrapper.draw` before writing the modified PDF.
271
+ The command maps input groups such as `text`, `image`, and `rectangle` to
272
+ raw element classes, validates the input against the CLI raw element schema,
273
+ creates the corresponding drawable objects, and calls `PdfWrapper.draw`
274
+ before writing the modified PDF. A data file can define multiple elements
275
+ grouped by type. Without `--file`, `--type` and the element options define
276
+ one element. The data file takes precedence when both input forms are
277
+ supplied.
253
278
 
254
279
  Args:
255
280
  ctx (typer.Context): Typer context containing global `PdfWrapper`
256
281
  options in `ctx.obj`.
257
282
  pdf (Path): Input PDF path.
258
- data (Path): JSON file containing grouped raw element definitions.
283
+ data (Path, optional): YAML or JSON file containing grouped raw element
284
+ definitions. Defaults to None.
259
285
  output (Path, optional): Output PDF path. If omitted, the input PDF is
260
286
  overwritten. Defaults to None.
287
+ element_type (str, optional): Element type used when `data` is omitted.
288
+ Defaults to None.
261
289
  """
262
290
  raw_element_map = {
263
291
  "text": RawElements.RawText,
@@ -274,36 +302,51 @@ def raw(
274
302
  schema=RAW_SCHEMA,
275
303
  method_name="draw",
276
304
  ctx=ctx,
277
- param_hint="--file",
305
+ file_param_hint="--file",
306
+ options_param_hint="element options",
278
307
  output=output,
308
+ element_type=element_type,
279
309
  )
280
310
 
281
311
 
282
312
  @create_cli.command(
313
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
283
314
  no_args_is_help=True,
284
- help="Add annotations to a PDF.",
315
+ help="Add annotations from a YAML or JSON file or command-line options.",
285
316
  )
286
317
  def annotation(
287
318
  ctx: typer.Context,
288
319
  pdf: INPUT_PDF,
289
- data: Annotated[Path, json_file_option("JSON file with annotation definitions.")],
320
+ data: Annotated[
321
+ Path | None, data_file_option("YAML or JSON file with annotation definitions.")
322
+ ] = None,
290
323
  output: OPTIONAL_OUTPUT_PDF = None,
324
+ element_type: Annotated[
325
+ str | None,
326
+ typer.Option("--type", help="Type of annotation to add without --file."),
327
+ ] = None,
291
328
  ) -> None:
292
329
  """
293
- Add annotations described by grouped JSON definitions.
330
+ Add annotations from a data file or command-line options.
294
331
 
295
- The command maps JSON groups such as `text`, `link`, and `highlight` to
296
- annotation classes, validates the input file against the CLI annotation
297
- schema, creates the corresponding annotation objects, and calls
298
- `PdfWrapper.annotate` before writing the modified PDF.
332
+ The command maps input groups such as `text`, `link`, and `highlight` to
333
+ annotation classes, validates the input against the CLI annotation schema,
334
+ creates the corresponding annotation objects, and calls
335
+ `PdfWrapper.annotate` before writing the modified PDF. A data file can
336
+ define multiple annotations grouped by type. Without `--file`, `--type` and
337
+ the annotation options define one annotation. The data file takes
338
+ precedence when both input forms are supplied.
299
339
 
300
340
  Args:
301
341
  ctx (typer.Context): Typer context containing global `PdfWrapper`
302
342
  options in `ctx.obj`.
303
343
  pdf (Path): Input PDF path.
304
- data (Path): JSON file containing grouped annotation definitions.
344
+ data (Path, optional): YAML or JSON file containing grouped annotation
345
+ definitions. Defaults to None.
305
346
  output (Path, optional): Output PDF path. If omitted, the input PDF is
306
347
  overwritten. Defaults to None.
348
+ element_type (str, optional): Annotation type used when `data` is
349
+ omitted. Defaults to None.
307
350
  """
308
351
  annotation_map = {
309
352
  "text": Annotations.TextAnnotation,
@@ -321,8 +364,10 @@ def annotation(
321
364
  schema=ANNOTATION_SCHEMA,
322
365
  method_name="annotate",
323
366
  ctx=ctx,
324
- param_hint="--file",
367
+ file_param_hint="--file",
368
+ options_param_hint="annotation options",
325
369
  output=output,
370
+ element_type=element_type,
326
371
  )
327
372
 
328
373
 
@@ -10,7 +10,7 @@ of an import traceback when those optional dependencies are absent.
10
10
  import importlib
11
11
  import sys
12
12
 
13
- CLI_DEPENDENCIES = {"jsonschema", "typer"}
13
+ CLI_DEPENDENCIES = {"jsonschema", "typer", "yaml"}
14
14
  CLI_INSTALL_HINT = 'pipx install "PyPDFForm[cli]"'
15
15
  CLI_ENV_INSTALL_HINT = "pip install 'PyPDFForm[cli]'"
16
16