PyPDFForm 5.3.0__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 (86) hide show
  1. {pypdfform-5.3.0 → pypdfform-5.4.0}/PKG-INFO +1 -13
  2. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/common.py +124 -28
  4. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/create.py +66 -30
  5. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/root.py +24 -10
  6. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/update.py +90 -24
  7. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/PKG-INFO +1 -13
  8. {pypdfform-5.3.0 → pypdfform-5.4.0}/README.md +0 -12
  9. {pypdfform-5.3.0 → pypdfform-5.4.0}/LICENSE +0 -0
  10. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/__init__.py +0 -0
  11. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/entry.py +0 -0
  12. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/inspect.py +0 -0
  13. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/remove.py +0 -0
  14. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/__init__.py +0 -0
  15. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/create.py +0 -0
  16. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/update.py +0 -0
  17. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/__init__.py +0 -0
  18. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/adapter.py +0 -0
  19. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/__init__.py +0 -0
  20. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/base.py +0 -0
  21. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/link.py +0 -0
  22. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/stamp.py +0 -0
  23. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/text.py +0 -0
  24. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  25. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/assets/__init__.py +0 -0
  26. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/assets/bedrock.py +0 -0
  27. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/assets/blank.py +0 -0
  28. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/constants.py +0 -0
  29. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/coordinate.py +0 -0
  30. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/deprecation.py +0 -0
  31. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/egress.py +0 -0
  32. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/filler.py +0 -0
  33. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/font.py +0 -0
  34. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/hooks.py +0 -0
  35. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/image.py +0 -0
  36. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/__init__.py +0 -0
  37. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/base.py +0 -0
  38. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  39. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  40. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/image.py +0 -0
  41. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/radio.py +0 -0
  42. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/signature.py +0 -0
  43. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/text.py +0 -0
  44. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/patterns.py +0 -0
  45. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/__init__.py +0 -0
  46. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/circle.py +0 -0
  47. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/ellipse.py +0 -0
  48. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/image.py +0 -0
  49. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/line.py +0 -0
  50. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/rect.py +0 -0
  51. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/text.py +0 -0
  52. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/template.py +0 -0
  53. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/types.py +0 -0
  54. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/utils.py +0 -0
  55. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/watermark.py +0 -0
  56. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/__init__.py +0 -0
  57. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/base.py +0 -0
  58. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  59. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  60. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/image.py +0 -0
  61. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/radio.py +0 -0
  62. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/signature.py +0 -0
  63. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/text.py +0 -0
  64. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/wrapper.py +0 -0
  65. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  66. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  67. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/entry_points.txt +0 -0
  68. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/requires.txt +0 -0
  69. {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/top_level.txt +0 -0
  70. {pypdfform-5.3.0 → pypdfform-5.4.0}/pyproject.toml +0 -0
  71. {pypdfform-5.3.0 → pypdfform-5.4.0}/setup.cfg +0 -0
  72. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_bulk_create_fields.py +0 -0
  73. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_create_widget.py +0 -0
  74. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_draw_elements.py +0 -0
  75. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_dropdown.py +0 -0
  76. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_extract_middleware_attributes.py +0 -0
  77. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_fill_max_length_text_field.py +0 -0
  78. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_font_widths.py +0 -0
  79. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_functional.py +0 -0
  80. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_generate_appearance_streams.py +0 -0
  81. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_js.py +0 -0
  82. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_need_appearances.py +0 -0
  83. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_paragraph.py +0 -0
  84. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_signature.py +0 -0
  85. {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_use_full_widget_name.py +0 -0
  86. {pypdfform-5.3.0 → 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.3.0
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
@@ -121,15 +121,3 @@ The official documentation can be found on [the GitHub page](https://chinapandam
121
121
  <!-- TODO: remove WIP when finish recording -->
122
122
  * [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
123
123
  * [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
124
-
125
- ## Star History
126
-
127
- 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.
128
-
129
- <a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
130
- <picture>
131
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
132
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
133
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
134
- </picture>
135
- </a>
@@ -18,7 +18,7 @@ from Python code or from the command line.
18
18
 
19
19
  import logging
20
20
 
21
- __version__ = "5.3.0"
21
+ __version__ = "5.4.0"
22
22
 
23
23
  from .lib.annotations import Annotations
24
24
  from .lib.assets.blank import BlankPage
@@ -137,6 +137,41 @@ def _input_file_kind(data: Path) -> str:
137
137
  return "JSON"
138
138
 
139
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
+
140
175
  def load_data_file(data: Path, schema: dict, param_hint: str) -> Any:
141
176
  """
142
177
  Loads a YAML or JSON CLI input file and validates it against a schema.
@@ -164,18 +199,53 @@ def load_data_file(data: Path, schema: dict, param_hint: str) -> Any:
164
199
  cause=exc,
165
200
  )
166
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
+
167
236
  try:
168
- validate(instance=input_data, schema=schema)
169
- except ValidationError as exc:
170
- error_path = _validation_error_path(exc)
171
- location = f" at {error_path}" if error_path else ""
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:
172
242
  cli_bad_parameter(
173
- f"Invalid {input_kind} file{location}: {exc.message}",
243
+ "Use '--name value' pairs with valid values.",
174
244
  param_hint=param_hint,
175
245
  cause=exc,
176
246
  )
177
247
 
178
- return input_data
248
+ return _validate_input_data(input_data, schema, "CLI options", param_hint)
179
249
 
180
250
 
181
251
  def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
@@ -209,62 +279,88 @@ def handle_font_registration(
209
279
  """
210
280
  Registers a custom font referenced by CLI input.
211
281
 
212
- CLI input files may provide a file path in a `font` parameter. This helper
282
+ CLI input may provide a file path in a `font` parameter. This helper
213
283
  registers each unique font path on the supplied `PdfWrapper` once, assigns
214
284
  it a generated internal font name, and mutates `params["font"]` to that
215
285
  registered name so downstream field or element constructors can use it.
216
286
 
217
287
  Args:
218
288
  obj (PdfWrapper): The wrapper for the PDF currently being modified.
219
- params (dict): The element or widget parameters loaded from the input
220
- file. This dictionary is mutated when it contains a `font` key.
221
- registered_font (dict): Mapping of source font paths to generated
222
- `PdfWrapper` font names for the current command invocation.
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.
223
294
  """
224
295
  if "font" in params:
225
- if params["font"] not in registered_font:
296
+ font_path = str(Path(params["font"]).resolve())
297
+ if font_path not in registered_font:
226
298
  font_name = f"new_font_{len(registered_font)}"
227
- obj.register_font(font_name, params["font"])
228
- registered_font[params["font"]] = font_name
229
- params["font"] = registered_font[params["font"]]
299
+ obj.register_font(font_name, font_path)
300
+ registered_font[font_path] = font_name
301
+ params["font"] = registered_font[font_path]
230
302
 
231
303
 
232
304
  def create_elements_from_file(
233
305
  pdf: Path,
234
- data: Path,
306
+ data: Path | None,
235
307
  element_map: dict,
236
308
  schema: dict,
237
309
  method_name: str,
238
310
  ctx: typer.Context,
239
- param_hint: str,
311
+ file_param_hint: str,
312
+ options_param_hint: str,
240
313
  output: Path | None = None,
314
+ element_type: str | None = None,
241
315
  ) -> None:
242
316
  """
243
- Creates PDF elements from grouped file definitions.
317
+ Creates PDF elements from a data file or command-line options.
244
318
 
245
- The input data is expected to group element definitions by type, such as
246
- `text`, `image`, or `highlight`. Each group key is resolved through
247
- `element_map`, each item is constructed after optional font registration,
248
- and the resulting objects are passed to `method_name` on `PdfWrapper`.
249
- The modified PDF is written to `output` or back to the input path.
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`.
250
325
 
251
326
  Args:
252
327
  pdf (Path): The path to the input PDF file.
253
- data (Path): The path to the input file containing grouped element
254
- definitions.
328
+ data (Path, optional): The path to the input file containing grouped
329
+ element definitions. Defaults to None.
255
330
  element_map (dict): Mapping from input group names to element classes
256
331
  or callables used to construct each object.
257
- schema (dict): JSON schema used to validate the grouped definitions.
332
+ schema (dict): JSON schema used to validate file or command-line input.
258
333
  method_name (str): Name of the `PdfWrapper` method that accepts the
259
334
  constructed elements, such as `bulk_create_fields`, `draw`, or
260
335
  `annotate`.
261
336
  ctx (typer.Context): Typer context containing global wrapper options in
262
337
  `ctx.obj`.
263
- param_hint (str): CLI parameter associated with the input file.
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.
264
341
  output (Path, optional): Path where the modified PDF should be saved. If
265
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.
266
345
  """
267
- input_data = load_data_file(data, schema, param_hint)
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
+ }
268
364
 
269
365
  obj = PdfWrapper(str(pdf), **ctx.obj)
270
366
  ungrouped_input = []
@@ -188,33 +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
198
  data: Annotated[
198
- Path, data_file_option("YAML or JSON file with form field definitions.")
199
- ],
199
+ Path | None, data_file_option("YAML or JSON file with form field definitions.")
200
+ ] = None,
200
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,
201
206
  ) -> None:
202
207
  """
203
- Add form fields described by grouped YAML or JSON definitions.
208
+ Add form fields from a data file or command-line options.
204
209
 
205
210
  The command maps input groups such as `text`, `check`, and `signature` to
206
- PyPDFForm field classes, validates the input file against the CLI field
207
- schema, creates the corresponding field objects, and calls
208
- `PdfWrapper.bulk_create_fields` before writing the modified PDF.
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.
209
217
 
210
218
  Args:
211
219
  ctx (typer.Context): Typer context containing global `PdfWrapper`
212
220
  options in `ctx.obj`.
213
221
  pdf (Path): Input PDF path.
214
- data (Path): YAML or JSON file containing grouped form field
215
- definitions.
222
+ data (Path, optional): YAML or JSON file containing grouped form field
223
+ definitions. Defaults to None.
216
224
  output (Path, optional): Output PDF path. If omitted, the input PDF is
217
225
  overwritten. Defaults to None.
226
+ element_type (str, optional): Form field type used when `data` is
227
+ omitted. Defaults to None.
218
228
  """
219
229
  field_map = {
220
230
  "text": Fields.TextField,
@@ -231,39 +241,51 @@ def field(
231
241
  schema=FIELD_SCHEMA,
232
242
  method_name="bulk_create_fields",
233
243
  ctx=ctx,
234
- param_hint="--file",
244
+ file_param_hint="--file",
245
+ options_param_hint="field options",
235
246
  output=output,
247
+ element_type=element_type,
236
248
  )
237
249
 
238
250
 
239
251
  @create_cli.command(
252
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
240
253
  no_args_is_help=True,
241
- help="Draw text, images, and shapes on a PDF.",
254
+ help="Draw elements from a YAML or JSON file or command-line options.",
242
255
  )
243
256
  def raw(
244
257
  ctx: typer.Context,
245
258
  pdf: INPUT_PDF,
246
259
  data: Annotated[
247
- Path, data_file_option("YAML or JSON file with raw element definitions.")
248
- ],
260
+ Path | None, data_file_option("YAML or JSON file with raw element definitions.")
261
+ ] = None,
249
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,
250
267
  ) -> None:
251
268
  """
252
- Draw raw elements described by grouped YAML or JSON definitions.
269
+ Draw elements from a data file or command-line options.
253
270
 
254
271
  The command maps input groups such as `text`, `image`, and `rectangle` to
255
- raw element classes, validates the input file against the CLI raw element
256
- schema, creates the corresponding drawable objects, and calls
257
- `PdfWrapper.draw` before writing the modified PDF.
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.
258
278
 
259
279
  Args:
260
280
  ctx (typer.Context): Typer context containing global `PdfWrapper`
261
281
  options in `ctx.obj`.
262
282
  pdf (Path): Input PDF path.
263
- data (Path): YAML or JSON file containing grouped raw element
264
- definitions.
283
+ data (Path, optional): YAML or JSON file containing grouped raw element
284
+ definitions. Defaults to None.
265
285
  output (Path, optional): Output PDF path. If omitted, the input PDF is
266
286
  overwritten. Defaults to None.
287
+ element_type (str, optional): Element type used when `data` is omitted.
288
+ Defaults to None.
267
289
  """
268
290
  raw_element_map = {
269
291
  "text": RawElements.RawText,
@@ -280,39 +302,51 @@ def raw(
280
302
  schema=RAW_SCHEMA,
281
303
  method_name="draw",
282
304
  ctx=ctx,
283
- param_hint="--file",
305
+ file_param_hint="--file",
306
+ options_param_hint="element options",
284
307
  output=output,
308
+ element_type=element_type,
285
309
  )
286
310
 
287
311
 
288
312
  @create_cli.command(
313
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
289
314
  no_args_is_help=True,
290
- help="Add annotations to a PDF.",
315
+ help="Add annotations from a YAML or JSON file or command-line options.",
291
316
  )
292
317
  def annotation(
293
318
  ctx: typer.Context,
294
319
  pdf: INPUT_PDF,
295
320
  data: Annotated[
296
- Path, data_file_option("YAML or JSON file with annotation definitions.")
297
- ],
321
+ Path | None, data_file_option("YAML or JSON file with annotation definitions.")
322
+ ] = None,
298
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,
299
328
  ) -> None:
300
329
  """
301
- Add annotations described by grouped YAML or JSON definitions.
330
+ Add annotations from a data file or command-line options.
302
331
 
303
332
  The command maps input groups such as `text`, `link`, and `highlight` to
304
- annotation classes, validates the input file against the CLI annotation
305
- schema, creates the corresponding annotation objects, and calls
306
- `PdfWrapper.annotate` before writing the modified PDF.
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.
307
339
 
308
340
  Args:
309
341
  ctx (typer.Context): Typer context containing global `PdfWrapper`
310
342
  options in `ctx.obj`.
311
343
  pdf (Path): Input PDF path.
312
- data (Path): YAML or JSON file containing grouped annotation
313
- definitions.
344
+ data (Path, optional): YAML or JSON file containing grouped annotation
345
+ definitions. Defaults to None.
314
346
  output (Path, optional): Output PDF path. If omitted, the input PDF is
315
347
  overwritten. Defaults to None.
348
+ element_type (str, optional): Annotation type used when `data` is
349
+ omitted. Defaults to None.
316
350
  """
317
351
  annotation_map = {
318
352
  "text": Annotations.TextAnnotation,
@@ -330,8 +364,10 @@ def annotation(
330
364
  schema=ANNOTATION_SCHEMA,
331
365
  method_name="annotate",
332
366
  ctx=ctx,
333
- param_hint="--file",
367
+ file_param_hint="--file",
368
+ options_param_hint="annotation options",
334
369
  output=output,
370
+ element_type=element_type,
335
371
  )
336
372
 
337
373
 
@@ -8,7 +8,7 @@ commands. The root callback collects global flags in the Typer context so each
8
8
  subcommand can initialize `PdfWrapper` with consistent settings.
9
9
 
10
10
  Commands:
11
- - `fill`: Fill an existing PDF form from YAML or JSON data.
11
+ - `fill`: Fill an existing PDF form from a data file or field options.
12
12
  - `create`: Create PDFs, fields, annotations, raw elements, and grid views.
13
13
  - `inspect`: Print form metadata and field data as JSON.
14
14
  - `update`: Modify PDF metadata, field names, properties, geometry, and scripts.
@@ -21,7 +21,13 @@ from typing import Annotated
21
21
  import typer
22
22
 
23
23
  from .. import PdfWrapper, Widgets, __version__
24
- from .common import INPUT_PDF, OPTIONAL_OUTPUT_PDF, data_file_option, load_data_file
24
+ from .common import (
25
+ INPUT_PDF,
26
+ OPTIONAL_OUTPUT_PDF,
27
+ data_file_option,
28
+ load_data_file,
29
+ load_data_options,
30
+ )
25
31
  from .create import create_cli
26
32
  from .inspect import inspect_cli
27
33
  from .remove import remove_cli
@@ -144,15 +150,16 @@ def main(
144
150
 
145
151
 
146
152
  @cli_app.command(
153
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
147
154
  no_args_is_help=True,
148
- help="Fill a PDF form with YAML or JSON data.",
155
+ help="Fill a PDF form from a YAML or JSON file or command-line options.",
149
156
  )
150
157
  def fill(
151
158
  ctx: typer.Context,
152
159
  pdf: INPUT_PDF,
153
160
  data: Annotated[
154
- Path, data_file_option("YAML or JSON file with form field values.")
155
- ],
161
+ Path | None, data_file_option("YAML or JSON file with form field values.")
162
+ ] = None,
156
163
  output: OPTIONAL_OUTPUT_PDF = None,
157
164
  flatten: Annotated[
158
165
  bool,
@@ -160,19 +167,22 @@ def fill(
160
167
  ] = None,
161
168
  ) -> None:
162
169
  """
163
- Fill an existing PDF form from a validated YAML or JSON file.
170
+ Fill an existing PDF form from a data file or command-line options.
164
171
 
165
172
  The command loads the input PDF with the global options stored by the root
166
173
  callback, expands the generated schema so image and signature widgets can
167
- accept path objects, validates the input file, normalizes image and
174
+ accept path objects, validates the input data, normalizes image and
168
175
  signature values, and writes the filled PDF to the requested output path or
169
- back to the input file.
176
+ back to the input file. When both a data file and field options are supplied,
177
+ the data file takes precedence.
170
178
 
171
179
  Args:
172
180
  ctx (typer.Context): Typer context containing global `PdfWrapper`
173
181
  options in `ctx.obj`.
174
182
  pdf (Path): Input PDF form path.
175
- data (Path): YAML or JSON file containing form field values.
183
+ data (Path, optional): YAML or JSON file containing form field values.
184
+ When omitted, form field values are read from the command-line
185
+ options in ``ctx.args``. Defaults to None.
176
186
  output (Path, optional): Output PDF path. If omitted, the input PDF is
177
187
  overwritten. Defaults to None.
178
188
  flatten (bool, optional): Whether to flatten form fields after filling.
@@ -198,7 +208,11 @@ def fill(
198
208
  ]
199
209
  }
200
210
 
201
- input_data = load_data_file(data, schema, "--file")
211
+ input_data = (
212
+ load_data_file(data, schema, "--file")
213
+ if data is not None
214
+ else load_data_options(ctx.args, schema, "form field options")
215
+ )
202
216
  for k, each in obj.widgets.items():
203
217
  if (
204
218
  k in input_data
@@ -26,6 +26,7 @@ from .common import (
26
26
  get_widget,
27
27
  handle_font_registration,
28
28
  load_data_file,
29
+ load_data_options,
29
30
  )
30
31
  from .schemas.update import FIELD_SCHEMA, RENAME_SCHEMA
31
32
 
@@ -185,33 +186,46 @@ def bounds(
185
186
 
186
187
 
187
188
  @update_cli.command(
189
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
188
190
  no_args_is_help=True,
189
- help="Rename form fields from YAML or JSON.",
191
+ help="Rename form fields from YAML or JSON or command-line options.",
190
192
  )
191
193
  def rename(
192
194
  ctx: typer.Context,
193
195
  pdf: INPUT_PDF,
194
196
  data: Annotated[
195
- Path, data_file_option("YAML or JSON file with form field renames.")
196
- ],
197
+ Path | None, data_file_option("YAML or JSON file with form field renames.")
198
+ ] = None,
197
199
  output: OPTIONAL_OUTPUT_PDF = None,
200
+ widget: Annotated[
201
+ str | None,
202
+ typer.Option(
203
+ "--field",
204
+ help="Form field name to rename without --file.",
205
+ ),
206
+ ] = None,
198
207
  ) -> None:
199
208
  """
200
- Rename form fields using a validated YAML or JSON mapping file.
209
+ Rename form fields from a data file or command-line options.
201
210
 
202
- The command rejects full widget name mode, validates the input file against
203
- the rename schema, resolves each existing field, queues the requested key
211
+ The command rejects full widget name mode, validates the input against the
212
+ rename schema, resolves each existing field, queues the requested key
204
213
  updates on the `PdfWrapper`, commits the rename operations, and writes the
205
- modified PDF to the requested output path or back to the input file.
214
+ modified PDF to the requested output path or back to the input file. A data
215
+ file can rename multiple fields. Without `--file`, `--field` and the rename
216
+ options update one field. The data file takes precedence when both input
217
+ forms are supplied.
206
218
 
207
219
  Args:
208
220
  ctx (typer.Context): Typer context containing global `PdfWrapper`
209
221
  options in `ctx.obj`.
210
222
  pdf (Path): Input PDF path.
211
- data (Path): YAML or JSON file containing form field rename
212
- definitions.
223
+ data (Path, optional): YAML or JSON file containing form field rename
224
+ definitions. Defaults to None.
213
225
  output (Path, optional): Output PDF path. If omitted, the input PDF is
214
226
  overwritten. Defaults to None.
227
+ widget (str, optional): Form field name used when `data` is omitted.
228
+ Defaults to None.
215
229
 
216
230
  Raises:
217
231
  typer.BadParameter: Raised when full widget name mode is enabled, the
@@ -224,59 +238,111 @@ def rename(
224
238
  param_hint="--use-full-widget-name",
225
239
  )
226
240
 
227
- input_data = load_data_file(data, RENAME_SCHEMA, "--file")
241
+ if data is not None:
242
+ input_data = load_data_file(data, RENAME_SCHEMA, "--file")
243
+ field_param_hint = "--file"
244
+ else:
245
+ if widget is None:
246
+ cli_bad_parameter(
247
+ "Specify a form field when --file is omitted.",
248
+ param_hint="--field",
249
+ )
250
+ input_data = [
251
+ {
252
+ widget: load_data_options(
253
+ ctx.args,
254
+ RENAME_SCHEMA["items"]["additionalProperties"],
255
+ "rename options",
256
+ )
257
+ }
258
+ ]
259
+ field_param_hint = "--field"
228
260
 
229
261
  obj = PdfWrapper(str(pdf), **ctx.obj)
230
262
  for item in input_data:
231
263
  for k, v in item.items():
232
- widget = get_widget(obj, k, "--file")
233
- obj.update_widget_key(widget.name, v["new_key"], index=v.get("index", 0))
264
+ field_widget = get_widget(obj, k, field_param_hint)
265
+ obj.update_widget_key(
266
+ field_widget.name,
267
+ v["new_key"],
268
+ index=v.get("index", 0),
269
+ )
234
270
 
235
271
  obj.commit_widget_key_updates().write(output or pdf)
236
272
 
237
273
 
238
274
  @update_cli.command(
275
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
239
276
  no_args_is_help=True,
240
- help="Update form field properties from YAML or JSON.",
277
+ help="Update form field properties from YAML or JSON or command-line options.",
241
278
  )
242
279
  def field(
243
280
  ctx: typer.Context,
244
281
  pdf: INPUT_PDF,
245
282
  data: Annotated[
246
- Path, data_file_option("YAML or JSON file with form field property updates.")
247
- ],
283
+ Path | None,
284
+ data_file_option("YAML or JSON file with form field property updates."),
285
+ ] = None,
248
286
  output: OPTIONAL_OUTPUT_PDF = None,
287
+ widget: Annotated[
288
+ str | None,
289
+ typer.Option(
290
+ "--field",
291
+ help="Form field name to update without --file.",
292
+ ),
293
+ ] = None,
249
294
  ) -> None:
250
295
  """
251
- Update form field properties from a validated YAML or JSON file.
296
+ Update form field properties from a data file or command-line options.
252
297
 
253
- The command validates the input file against the field update schema, loads
254
- the PDF with the global CLI options stored in `ctx.obj`, resolves each
255
- field name, registers any referenced fonts once per invocation, assigns the
298
+ The command validates the input against the field update schema, loads the
299
+ PDF with the global CLI options stored in `ctx.obj`, resolves each field
300
+ name, registers any referenced fonts once per invocation, assigns the
256
301
  requested widget properties, and writes the modified PDF to the requested
257
- output path or back to the input file.
302
+ output path or back to the input file. A data file can update multiple
303
+ fields. Without `--file`, `--field` and the property options update one
304
+ field. The data file takes precedence when both input forms are supplied.
258
305
 
259
306
  Args:
260
307
  ctx (typer.Context): Typer context containing global `PdfWrapper`
261
308
  options in `ctx.obj`.
262
309
  pdf (Path): Input PDF path.
263
- data (Path): YAML or JSON file containing form field property updates.
310
+ data (Path, optional): YAML or JSON file containing form field property
311
+ updates. Defaults to None.
264
312
  output (Path, optional): Output PDF path. If omitted, the input PDF is
265
313
  overwritten. Defaults to None.
314
+ widget (str, optional): Form field name used when `data` is omitted.
315
+ Defaults to None.
266
316
 
267
317
  Raises:
268
318
  typer.BadParameter: Raised when the input file is invalid or a requested
269
319
  field does not exist.
270
320
  """
271
- input_data = load_data_file(data, FIELD_SCHEMA, "--file")
321
+ if data is not None:
322
+ input_data = load_data_file(data, FIELD_SCHEMA, "--file")
323
+ field_param_hint = "--file"
324
+ else:
325
+ if widget is None:
326
+ cli_bad_parameter(
327
+ "Specify a form field when --file is omitted.",
328
+ param_hint="--field",
329
+ )
330
+ input_data = {
331
+ widget: load_data_options(
332
+ ctx.args,
333
+ FIELD_SCHEMA["patternProperties"][".+"],
334
+ "field update options",
335
+ )
336
+ }
337
+ field_param_hint = "--field"
272
338
 
273
339
  obj = PdfWrapper(str(pdf), **ctx.obj)
274
340
  registered_font = {}
275
341
  for k, each in input_data.items():
276
- widget = get_widget(obj, k, "--file")
342
+ field_widget = get_widget(obj, k, field_param_hint)
277
343
  handle_font_registration(obj, each, registered_font)
278
344
  for param, v in each.items():
279
- setattr(widget, param, v)
345
+ setattr(field_widget, param, v)
280
346
 
281
347
  obj.write(output or pdf)
282
348
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 5.3.0
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
@@ -121,15 +121,3 @@ The official documentation can be found on [the GitHub page](https://chinapandam
121
121
  <!-- TODO: remove WIP when finish recording -->
122
122
  * [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
123
123
  * [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
124
-
125
- ## Star History
126
-
127
- 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.
128
-
129
- <a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
130
- <picture>
131
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
132
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
133
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
134
- </picture>
135
- </a>
@@ -72,15 +72,3 @@ The official documentation can be found on [the GitHub page](https://chinapandam
72
72
  <!-- TODO: remove WIP when finish recording -->
73
73
  * [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
74
74
  * [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
75
-
76
- ## Star History
77
-
78
- 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.
79
-
80
- <a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
81
- <picture>
82
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
83
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
84
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
85
- </picture>
86
- </a>
File without changes
File without changes
File without changes
File without changes