PyPDFForm 4.7.10__tar.gz → 4.8.1__tar.gz

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