PyPDFForm 4.8.1__tar.gz → 4.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {pypdfform-4.8.1 → pypdfform-4.8.2}/PKG-INFO +2 -2
  2. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/cli/__init__.py +28 -7
  4. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/cli/common.py +62 -5
  5. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/cli/create.py +31 -3
  6. pypdfform-4.8.2/PyPDFForm/cli/schemas/create.py +525 -0
  7. pypdfform-4.8.2/PyPDFForm/cli/schemas/update.py +74 -0
  8. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/cli/update.py +11 -8
  9. pypdfform-4.8.2/PyPDFForm/lib/assets/__init__.py +0 -0
  10. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/dropdown.py +8 -3
  11. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/wrapper.py +1 -0
  12. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm.egg-info/PKG-INFO +2 -2
  13. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm.egg-info/SOURCES.txt +3 -0
  14. {pypdfform-4.8.1 → pypdfform-4.8.2}/pyproject.toml +1 -2
  15. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_dropdown.py +3 -1
  16. {pypdfform-4.8.1 → pypdfform-4.8.2}/LICENSE +0 -0
  17. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/cli/inspect.py +0 -0
  18. {pypdfform-4.8.1/PyPDFForm/lib → pypdfform-4.8.2/PyPDFForm/cli/schemas}/__init__.py +0 -0
  19. {pypdfform-4.8.1/PyPDFForm/lib/assets → pypdfform-4.8.2/PyPDFForm/lib}/__init__.py +0 -0
  20. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/adapter.py +0 -0
  21. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/__init__.py +0 -0
  22. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/base.py +0 -0
  23. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/link.py +0 -0
  24. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/stamp.py +0 -0
  25. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/text.py +0 -0
  26. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  27. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/assets/bedrock.py +0 -0
  28. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/assets/blank.py +0 -0
  29. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/constants.py +0 -0
  30. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/coordinate.py +0 -0
  31. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/deprecation.py +0 -0
  32. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/egress.py +0 -0
  33. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/filler.py +0 -0
  34. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/font.py +0 -0
  35. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/hooks.py +0 -0
  36. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/image.py +0 -0
  37. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/__init__.py +0 -0
  38. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/base.py +0 -0
  39. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  40. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/image.py +0 -0
  41. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/radio.py +0 -0
  42. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/signature.py +0 -0
  43. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/middleware/text.py +0 -0
  44. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/patterns.py +0 -0
  45. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/raw/__init__.py +0 -0
  46. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/raw/circle.py +0 -0
  47. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/raw/ellipse.py +0 -0
  48. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/raw/image.py +0 -0
  49. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/raw/line.py +0 -0
  50. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/raw/rect.py +0 -0
  51. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/raw/text.py +0 -0
  52. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/template.py +0 -0
  53. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/types.py +0 -0
  54. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/utils.py +0 -0
  55. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/watermark.py +0 -0
  56. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/__init__.py +0 -0
  57. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/base.py +0 -0
  58. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  59. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  60. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/image.py +0 -0
  61. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/radio.py +0 -0
  62. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/signature.py +0 -0
  63. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm/lib/widgets/text.py +0 -0
  64. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  65. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm.egg-info/entry_points.txt +0 -0
  66. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm.egg-info/requires.txt +1 -1
  67. {pypdfform-4.8.1 → pypdfform-4.8.2}/PyPDFForm.egg-info/top_level.txt +0 -0
  68. {pypdfform-4.8.1 → pypdfform-4.8.2}/README.md +0 -0
  69. {pypdfform-4.8.1 → pypdfform-4.8.2}/setup.cfg +0 -0
  70. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_bulk_create_fields.py +0 -0
  71. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_create_widget.py +0 -0
  72. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_draw_elements.py +0 -0
  73. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_extract_middleware_attributes.py +0 -0
  74. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_fill_max_length_text_field.py +0 -0
  75. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_font_widths.py +0 -0
  76. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_functional.py +0 -0
  77. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_generate_appearance_streams.py +0 -0
  78. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_js.py +0 -0
  79. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_need_appearances.py +0 -0
  80. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_paragraph.py +0 -0
  81. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_signature.py +0 -0
  82. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_use_full_widget_name.py +0 -0
  83. {pypdfform-4.8.1 → pypdfform-4.8.2}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.8.1
3
+ Version: 4.8.2
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -28,11 +28,11 @@ Requires-Dist: pypdf<7.0.0,>=6.10.1
28
28
  Requires-Dist: reportlab<5.0.0,>=4.4.6
29
29
  Provides-Extra: cli
30
30
  Requires-Dist: typer<1.0.0,>=0.24.1; extra == "cli"
31
+ Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "cli"
31
32
  Provides-Extra: dev
32
33
  Requires-Dist: black<27.0.0,>=26.3.1; extra == "dev"
33
34
  Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
34
35
  Requires-Dist: isort<9.0.0,>=8.0.1; extra == "dev"
35
- Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "dev"
36
36
  Requires-Dist: mike<3.0.0,>=2.1.3; extra == "dev"
37
37
  Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
38
38
  Requires-Dist: mkdocs-material<10.0.0,>=9.7.6; extra == "dev"
@@ -22,7 +22,7 @@ PyPDFForm aims to simplify PDF form manipulation, making it accessible to develo
22
22
 
23
23
  import logging
24
24
 
25
- __version__ = "4.8.1"
25
+ __version__ = "4.8.2"
26
26
 
27
27
  from .lib.annotations import Annotations
28
28
  from .lib.assets.blank import BlankPage
@@ -14,14 +14,14 @@ Commands:
14
14
  - `update`: Modify PDF metadata, field names, properties, geometry, and scripts.
15
15
  """
16
16
 
17
- import json
18
17
  from pathlib import Path
19
18
  from typing import Annotated
20
19
 
21
20
  import typer
22
21
 
23
22
  from .. import PdfWrapper, Widgets, __version__
24
- from .common import INPUT_PDF, OPTIONAL_OUTPUT_PDF, json_file_option
23
+ from .common import (INPUT_PDF, OPTIONAL_OUTPUT_PDF, json_file_option,
24
+ load_json_file)
25
25
  from .create import create_cli
26
26
  from .inspect import inspect_cli
27
27
  from .update import update_cli
@@ -132,13 +132,34 @@ def fill(
132
132
  ] = None,
133
133
  ) -> None:
134
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
135
  obj = PdfWrapper(str(pdf), **ctx.obj)
136
+
137
+ schema = obj.schema
138
+ for key, widget in obj.widgets.items():
139
+ if isinstance(widget, (Widgets.Image, Widgets.Signature)):
140
+ schema["properties"][key] = {
141
+ "anyOf": [
142
+ schema["properties"][key],
143
+ {
144
+ "type": "object",
145
+ "properties": {
146
+ "path": {"type": "string"},
147
+ "preserve_aspect_ratio": {"type": "boolean"},
148
+ },
149
+ "required": ["path"],
150
+ "additionalProperties": False,
151
+ },
152
+ ]
153
+ }
154
+
155
+ input_data = load_json_file(data, schema, "--file")
139
156
  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(
157
+ if (
158
+ k in input_data
159
+ and isinstance(each, (Widgets.Image, Widgets.Signature))
160
+ and isinstance(input_data[k], dict)
161
+ ):
162
+ each.preserve_aspect_ratio = input_data[k].get(
142
163
  "preserve_aspect_ratio", each.preserve_aspect_ratio
143
164
  )
144
165
  input_data[k] = input_data[k]["path"]
@@ -9,9 +9,10 @@ into the objects expected by `PdfWrapper` methods.
9
9
 
10
10
  import json
11
11
  from pathlib import Path
12
- from typing import Annotated, NoReturn
12
+ from typing import Annotated, Any, NoReturn
13
13
 
14
14
  import typer
15
+ from jsonschema import ValidationError, validate
15
16
 
16
17
  from .. import PdfWrapper
17
18
  from ..lib.middleware.base import Widget
@@ -76,7 +77,7 @@ def json_file_option(help_text: str):
76
77
  )
77
78
 
78
79
 
79
- def cli_bad_parameter(
80
+ def _cli_bad_parameter(
80
81
  message: str,
81
82
  param_hint: str,
82
83
  cause: BaseException,
@@ -95,6 +96,59 @@ def cli_bad_parameter(
95
96
  raise typer.BadParameter(message, param_hint=param_hint) from cause
96
97
 
97
98
 
99
+ def _validation_error_path(exc: ValidationError) -> str:
100
+ """
101
+ Builds a dotted JSON path for a validation error.
102
+
103
+ Args:
104
+ exc (ValidationError): The JSON schema validation error.
105
+
106
+ Returns:
107
+ str: Dotted path for the failing instance location.
108
+ """
109
+ return ".".join(str(each) for each in exc.absolute_path)
110
+
111
+
112
+ def load_json_file(data: Path, schema: dict, param_hint: str) -> Any:
113
+ """
114
+ Loads a JSON CLI input file and validates it against a schema.
115
+
116
+ Args:
117
+ data (Path): JSON file path.
118
+ schema (dict): JSON schema to validate against.
119
+ param_hint (str): CLI parameter associated with the JSON file.
120
+
121
+ Returns:
122
+ Any: Parsed and validated JSON input.
123
+
124
+ Raises:
125
+ typer.BadParameter: Raised when the file cannot be loaded or validation
126
+ fails.
127
+ """
128
+ try:
129
+ with open(data, "r", encoding="utf-8") as f:
130
+ input_data = json.load(f)
131
+ except (OSError, json.JSONDecodeError) as exc:
132
+ _cli_bad_parameter(
133
+ f"Invalid JSON file: {exc}",
134
+ param_hint=param_hint,
135
+ cause=exc,
136
+ )
137
+
138
+ try:
139
+ validate(instance=input_data, schema=schema)
140
+ except ValidationError as exc:
141
+ error_path = _validation_error_path(exc)
142
+ location = f" at {error_path}" if error_path else ""
143
+ _cli_bad_parameter(
144
+ f"Invalid JSON file{location}: {exc.message}",
145
+ param_hint=param_hint,
146
+ cause=exc,
147
+ )
148
+
149
+ return input_data
150
+
151
+
98
152
  def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
99
153
  """
100
154
  Look up a widget and report missing names as CLI input errors.
@@ -113,7 +167,7 @@ def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
113
167
  try:
114
168
  return wrapper.widgets[field]
115
169
  except KeyError as exc:
116
- cli_bad_parameter(
170
+ _cli_bad_parameter(
117
171
  f"Form field '{field}' does not exist.",
118
172
  param_hint=param_hint,
119
173
  cause=exc,
@@ -150,8 +204,10 @@ def create_elements_from_file(
150
204
  pdf: Path,
151
205
  data: Path,
152
206
  element_map: dict,
207
+ schema: dict,
153
208
  method_name: str,
154
209
  ctx: typer.Context,
210
+ param_hint: str,
155
211
  output: Path | None = None,
156
212
  ) -> None:
157
213
  """
@@ -169,16 +225,17 @@ def create_elements_from_file(
169
225
  definitions.
170
226
  element_map (dict): Mapping from JSON group names to element classes or
171
227
  callables used to construct each object.
228
+ schema (dict): JSON schema used to validate the grouped definitions.
172
229
  method_name (str): Name of the `PdfWrapper` method that accepts the
173
230
  constructed elements, such as `bulk_create_fields`, `draw`, or
174
231
  `annotate`.
175
232
  ctx (typer.Context): Typer context containing global wrapper options in
176
233
  `ctx.obj`.
234
+ param_hint (str): CLI parameter associated with the JSON file.
177
235
  output (Path, optional): Path where the modified PDF should be saved. If
178
236
  omitted, the input PDF is overwritten. Defaults to None.
179
237
  """
180
- with open(data, "r", encoding="utf-8") as f:
181
- input_data = json.load(f)
238
+ input_data = load_json_file(data, schema, param_hint)
182
239
 
183
240
  obj = PdfWrapper(str(pdf), **ctx.obj)
184
241
  ungrouped_input = []
@@ -18,6 +18,7 @@ from .. import (Annotations, BlankPage, Fields, PdfArray, PdfWrapper,
18
18
  RawElements)
19
19
  from .common import (INPUT_PDF, OPTIONAL_OUTPUT_PDF, REQUIRED_OUTPUT_PDF,
20
20
  create_elements_from_file, json_file_option)
21
+ from .schemas.create import ANNOTATION_SCHEMA, FIELD_SCHEMA, RAW_SCHEMA
21
22
 
22
23
  create_cli = typer.Typer(
23
24
  context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
@@ -132,7 +133,16 @@ def field(
132
133
  "image": Fields.ImageField,
133
134
  "signature": Fields.SignatureField,
134
135
  }
135
- create_elements_from_file(pdf, data, field_map, "bulk_create_fields", ctx, output)
136
+ create_elements_from_file(
137
+ pdf=pdf,
138
+ data=data,
139
+ element_map=field_map,
140
+ schema=FIELD_SCHEMA,
141
+ method_name="bulk_create_fields",
142
+ ctx=ctx,
143
+ param_hint="--file",
144
+ output=output,
145
+ )
136
146
 
137
147
 
138
148
  @create_cli.command(no_args_is_help=True)
@@ -151,7 +161,16 @@ def raw(
151
161
  "circle": RawElements.RawCircle,
152
162
  "ellipse": RawElements.RawEllipse,
153
163
  }
154
- create_elements_from_file(pdf, data, raw_element_map, "draw", ctx, output)
164
+ create_elements_from_file(
165
+ pdf=pdf,
166
+ data=data,
167
+ element_map=raw_element_map,
168
+ schema=RAW_SCHEMA,
169
+ method_name="draw",
170
+ ctx=ctx,
171
+ param_hint="--file",
172
+ output=output,
173
+ )
155
174
 
156
175
 
157
176
  @create_cli.command(no_args_is_help=True)
@@ -171,7 +190,16 @@ def annotation(
171
190
  "strikeout": Annotations.StrikeOutAnnotation,
172
191
  "stamp": Annotations.RubberStampAnnotation,
173
192
  }
174
- create_elements_from_file(pdf, data, annotation_map, "annotate", ctx, output)
193
+ create_elements_from_file(
194
+ pdf=pdf,
195
+ data=data,
196
+ element_map=annotation_map,
197
+ schema=ANNOTATION_SCHEMA,
198
+ method_name="annotate",
199
+ ctx=ctx,
200
+ param_hint="--file",
201
+ output=output,
202
+ )
175
203
 
176
204
 
177
205
  @create_cli.command(no_args_is_help=True)