PyPDFForm 4.8.2__tar.gz → 4.8.4__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 (95) hide show
  1. {pypdfform-4.8.2 → pypdfform-4.8.4}/PKG-INFO +13 -11
  2. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/__init__.py +1 -1
  3. pypdfform-4.8.4/PyPDFForm/api/__init__.py +28 -0
  4. pypdfform-4.8.4/PyPDFForm/api/common.py +101 -0
  5. pypdfform-4.8.4/PyPDFForm/api/create.py +75 -0
  6. pypdfform-4.8.4/PyPDFForm/api/inspect.py +127 -0
  7. pypdfform-4.8.4/PyPDFForm/api/root.py +20 -0
  8. pypdfform-4.8.4/PyPDFForm/api/update.py +56 -0
  9. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/common.py +27 -31
  10. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/create.py +9 -1
  11. pypdfform-4.8.4/PyPDFForm/cli/entry.py +37 -0
  12. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/inspect.py +5 -2
  13. pypdfform-4.8.2/PyPDFForm/cli/__init__.py → pypdfform-4.8.4/PyPDFForm/cli/root.py +0 -3
  14. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/schemas/create.py +0 -1
  15. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/schemas/update.py +0 -1
  16. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/update.py +13 -4
  17. pypdfform-4.8.4/PyPDFForm/lib/assets/__init__.py +0 -0
  18. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/image.py +0 -1
  19. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/rect.py +0 -1
  20. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/dropdown.py +0 -1
  21. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/signature.py +0 -1
  22. pypdfform-4.8.4/PyPDFForm/shared/__init__.py +0 -0
  23. pypdfform-4.8.4/PyPDFForm/shared/utils.py +42 -0
  24. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm.egg-info/PKG-INFO +13 -11
  25. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm.egg-info/SOURCES.txt +10 -0
  26. pypdfform-4.8.4/PyPDFForm.egg-info/entry_points.txt +2 -0
  27. pypdfform-4.8.4/PyPDFForm.egg-info/requires.txt +27 -0
  28. {pypdfform-4.8.2 → pypdfform-4.8.4}/pyproject.toml +15 -12
  29. pypdfform-4.8.2/PyPDFForm.egg-info/entry_points.txt +0 -2
  30. pypdfform-4.8.2/PyPDFForm.egg-info/requires.txt +0 -24
  31. {pypdfform-4.8.2 → pypdfform-4.8.4}/LICENSE +0 -0
  32. {pypdfform-4.8.2/PyPDFForm/cli/schemas → pypdfform-4.8.4/PyPDFForm/cli}/__init__.py +0 -0
  33. {pypdfform-4.8.2/PyPDFForm/lib → pypdfform-4.8.4/PyPDFForm/cli/schemas}/__init__.py +0 -0
  34. {pypdfform-4.8.2/PyPDFForm/lib/assets → pypdfform-4.8.4/PyPDFForm/lib}/__init__.py +0 -0
  35. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/adapter.py +0 -0
  36. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/__init__.py +0 -0
  37. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/base.py +0 -0
  38. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/link.py +0 -0
  39. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/stamp.py +0 -0
  40. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/text.py +0 -0
  41. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  42. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/assets/bedrock.py +0 -0
  43. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/assets/blank.py +0 -0
  44. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/constants.py +0 -0
  45. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/coordinate.py +0 -0
  46. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/deprecation.py +0 -0
  47. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/egress.py +0 -0
  48. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/filler.py +0 -0
  49. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/font.py +0 -0
  50. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/hooks.py +0 -0
  51. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/image.py +0 -0
  52. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/__init__.py +0 -0
  53. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/base.py +0 -0
  54. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  55. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  56. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/image.py +0 -0
  57. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/radio.py +0 -0
  58. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/signature.py +0 -0
  59. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/text.py +0 -0
  60. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/patterns.py +0 -0
  61. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/__init__.py +0 -0
  62. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/circle.py +0 -0
  63. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/ellipse.py +0 -0
  64. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/line.py +0 -0
  65. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/text.py +0 -0
  66. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/template.py +0 -0
  67. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/types.py +0 -0
  68. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/utils.py +0 -0
  69. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/watermark.py +0 -0
  70. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/__init__.py +0 -0
  71. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/base.py +0 -0
  72. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  73. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/image.py +0 -0
  74. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/radio.py +0 -0
  75. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/text.py +0 -0
  76. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/wrapper.py +0 -0
  77. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  78. {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm.egg-info/top_level.txt +0 -0
  79. {pypdfform-4.8.2 → pypdfform-4.8.4}/README.md +0 -0
  80. {pypdfform-4.8.2 → pypdfform-4.8.4}/setup.cfg +0 -0
  81. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_bulk_create_fields.py +0 -0
  82. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_create_widget.py +0 -0
  83. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_draw_elements.py +0 -0
  84. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_dropdown.py +0 -0
  85. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_extract_middleware_attributes.py +0 -0
  86. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_fill_max_length_text_field.py +0 -0
  87. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_font_widths.py +0 -0
  88. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_functional.py +0 -0
  89. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_generate_appearance_streams.py +0 -0
  90. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_js.py +0 -0
  91. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_need_appearances.py +0 -0
  92. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_paragraph.py +0 -0
  93. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_signature.py +0 -0
  94. {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_use_full_widget_name.py +0 -0
  95. {pypdfform-4.8.2 → pypdfform-4.8.4}/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.2
3
+ Version: 4.8.4
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -20,28 +20,30 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: cryptography<47.0.0,>=46.0.3
23
+ Requires-Dist: cryptography<49.0.0,>=48.0.0
24
24
  Requires-Dist: fonttools<5.0.0,>=4.62.1
25
- Requires-Dist: pikepdf<11.0.0,>=10.5.0
25
+ Requires-Dist: pikepdf<11.0.0,>=10.5.1
26
26
  Requires-Dist: pillow<13.0.0,>=12.2.0
27
- Requires-Dist: pypdf<7.0.0,>=6.10.1
28
- Requires-Dist: reportlab<5.0.0,>=4.4.6
27
+ Requires-Dist: pypdf<7.0.0,>=6.11.0
28
+ Requires-Dist: reportlab<5.0.0,>=4.5.0
29
29
  Provides-Extra: cli
30
- Requires-Dist: typer<1.0.0,>=0.24.1; extra == "cli"
30
+ Requires-Dist: typer<1.0.0,>=0.25.1; extra == "cli"
31
31
  Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "cli"
32
+ Provides-Extra: web-api
33
+ Requires-Dist: fastapi[standard]<1.0.0,>=0.136.1; extra == "web-api"
32
34
  Provides-Extra: dev
33
35
  Requires-Dist: black<27.0.0,>=26.3.1; extra == "dev"
34
- Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
36
+ Requires-Dist: coverage<8.0.0,>=7.14.0; extra == "dev"
35
37
  Requires-Dist: isort<9.0.0,>=8.0.1; extra == "dev"
36
- Requires-Dist: mike<3.0.0,>=2.1.3; extra == "dev"
38
+ Requires-Dist: mike<3.0.0,>=2.2.0; extra == "dev"
37
39
  Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
38
40
  Requires-Dist: mkdocs-material<10.0.0,>=9.7.6; extra == "dev"
39
41
  Requires-Dist: pudb<2026.0.0,>=2025.1.5; extra == "dev"
40
42
  Requires-Dist: pylint<5.0.0,>=4.0.5; extra == "dev"
41
- Requires-Dist: pyright<2.0.0,>=1.1.407; extra == "dev"
43
+ Requires-Dist: pyright<2.0.0,>=1.1.409; extra == "dev"
42
44
  Requires-Dist: pytest<10.0.0,>=9.0.3; extra == "dev"
43
- Requires-Dist: requests<3.0.0,>=2.33.1; extra == "dev"
44
- Requires-Dist: ruff<1.0.0,>=0.14.6; extra == "dev"
45
+ Requires-Dist: requests<3.0.0,>=2.34.0; extra == "dev"
46
+ Requires-Dist: ruff<1.0.0,>=0.15.12; extra == "dev"
45
47
  Dynamic: license-file
46
48
 
47
49
  <p align="center"><img src="https://github.com/chinapandaman/PyPDFForm/raw/master/docs/img/logo.png"></p>
@@ -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.2"
25
+ __version__ = "4.8.4"
26
26
 
27
27
  from .lib.annotations import Annotations
28
28
  from .lib.assets.blank import BlankPage
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ FastAPI application for the optional PyPDFForm web API.
4
+
5
+ The web API is intended to be PyPDFForm's third user-facing interface alongside
6
+ the Python library and CLI. As the API surface grows, its endpoints should
7
+ closely mirror the CLI workflows for creating, filling, inspecting, and updating
8
+ PDF forms, while exposing those operations over HTTP.
9
+ """
10
+
11
+ from fastapi import FastAPI
12
+
13
+ from .. import __version__
14
+ from .create import create_router
15
+ from .inspect import inspect_router
16
+ from .root import root_router
17
+ from .update import update_router
18
+
19
+ app = FastAPI(
20
+ title="PyPDFForm Web API",
21
+ summary="Create, fill, inspect, and update PDF forms.",
22
+ version=__version__,
23
+ )
24
+
25
+ app.include_router(root_router)
26
+ app.include_router(create_router)
27
+ app.include_router(inspect_router)
28
+ app.include_router(update_router)
@@ -0,0 +1,101 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module provides shared helpers for PyPDFForm web API routes.
4
+
5
+ It defines the PDF response class and common query parameter parsing used by
6
+ endpoint groups that construct `PdfWrapper` instances from uploaded PDF files.
7
+ """
8
+
9
+ from typing import NoReturn
10
+
11
+ from fastapi import HTTPException, Query, Response, status
12
+ from pydantic import BaseModel
13
+
14
+
15
+ class PdfResponse(Response):
16
+ """
17
+ FastAPI response class for PDF bytes.
18
+
19
+ Attributes:
20
+ media_type (str): Response media type for PDF content.
21
+ """
22
+
23
+ media_type = "application/pdf"
24
+
25
+
26
+ class PdfWrapperOptions(BaseModel):
27
+ """
28
+ Common `PdfWrapper` options accepted by web API endpoints.
29
+
30
+ These fields mirror the constructor options exposed by the Python library
31
+ and CLI so each route can opt into the same PDF handling behavior.
32
+
33
+ Attributes:
34
+ need_appearances (bool): Whether to set the PDF's NeedAppearances flag.
35
+ generate_appearance_streams (bool): Whether to generate appearance
36
+ streams for filled fields.
37
+ preserve_metadata (bool): Whether to preserve source PDF metadata.
38
+ use_full_widget_name (bool): Whether to use full widget names when
39
+ reading fields.
40
+ """
41
+
42
+ need_appearances: bool = False
43
+ generate_appearance_streams: bool = False
44
+ preserve_metadata: bool = False
45
+ use_full_widget_name: bool = False
46
+
47
+ def as_kwargs(self) -> dict:
48
+ """
49
+ Convert options into `PdfWrapper` keyword arguments.
50
+
51
+ Returns:
52
+ dict: Mapping of option names to values accepted by `PdfWrapper`.
53
+ """
54
+ return self.model_dump()
55
+
56
+
57
+ def pdf_wrapper_options(
58
+ need_appearances: bool = Query(False),
59
+ generate_appearance_streams: bool = Query(False),
60
+ preserve_metadata: bool = Query(False),
61
+ use_full_widget_name: bool = Query(False),
62
+ ) -> PdfWrapperOptions:
63
+ """
64
+ Build common `PdfWrapper` options from query parameters.
65
+
66
+ FastAPI uses this function as a dependency so routes can share a
67
+ consistent query parameter set.
68
+
69
+ Args:
70
+ need_appearances (bool): Whether to set the PDF's NeedAppearances flag.
71
+ generate_appearance_streams (bool): Whether to generate appearance
72
+ streams for filled fields.
73
+ preserve_metadata (bool): Whether to preserve source PDF metadata.
74
+ use_full_widget_name (bool): Whether to use full widget names when
75
+ reading fields.
76
+
77
+ Returns:
78
+ PdfWrapperOptions: Parsed options for constructing a `PdfWrapper`.
79
+ """
80
+ return PdfWrapperOptions(
81
+ need_appearances=need_appearances,
82
+ generate_appearance_streams=generate_appearance_streams,
83
+ preserve_metadata=preserve_metadata,
84
+ use_full_widget_name=use_full_widget_name,
85
+ )
86
+
87
+
88
+ def api_widget_key_error(message: str, cause: KeyError) -> NoReturn:
89
+ """
90
+ Raise a web API error for a missing form field.
91
+
92
+ Args:
93
+ message (str): Error message to return to the API client.
94
+ cause (KeyError): Original lookup error.
95
+
96
+ Raises:
97
+ HTTPException: Raised with a 404 response for the missing field.
98
+ """
99
+ raise HTTPException(
100
+ status_code=status.HTTP_404_NOT_FOUND, detail=message
101
+ ) from cause
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module defines web API routes for creating PDF files and PDF content.
4
+
5
+ It exposes `/create` endpoints that accept uploaded PDFs, apply matching
6
+ `PdfWrapper` creation operations, and return generated PDF bytes to HTTP
7
+ clients.
8
+ """
9
+
10
+ from typing import Annotated
11
+
12
+ from fastapi import APIRouter, Depends, File, Form, UploadFile
13
+
14
+ from .. import PdfWrapper
15
+ from .common import PdfResponse, PdfWrapperOptions, pdf_wrapper_options
16
+
17
+ create_router = APIRouter(prefix="/create", tags=["create"])
18
+
19
+
20
+ @create_router.post(
21
+ "/grid",
22
+ summary="Add a coordinate grid to a PDF.",
23
+ response_class=PdfResponse,
24
+ responses={
25
+ 200: {
26
+ "content": {
27
+ "application/pdf": {"schema": {"type": "string", "format": "binary"}}
28
+ },
29
+ }
30
+ },
31
+ )
32
+ def grid(
33
+ options: Annotated[PdfWrapperOptions, Depends(pdf_wrapper_options)],
34
+ pdf: Annotated[UploadFile, File()],
35
+ red: Annotated[float, Form()] = None,
36
+ green: Annotated[float, Form()] = None,
37
+ blue: Annotated[float, Form()] = None,
38
+ margin: Annotated[float, Form()] = None,
39
+ ) -> PdfResponse:
40
+ """
41
+ Upload a PDF and return a copy with a coordinate grid overlaid on each page.
42
+
43
+ Use the optional RGB components to choose the grid color and `margin` to
44
+ adjust the grid spacing from the page edges.
45
+
46
+ \f
47
+
48
+ Args:
49
+ options (PdfWrapperOptions): Common `PdfWrapper` construction options.
50
+ pdf (UploadFile): Uploaded PDF file to annotate with a grid.
51
+ red (float): Red component of the grid color, from 0 to 1.
52
+ green (float): Green component of the grid color, from 0 to 1.
53
+ blue (float): Blue component of the grid color, from 0 to 1.
54
+ margin (float): Grid margin in points.
55
+
56
+ Returns:
57
+ PdfResponse: PDF response containing the document with a coordinate grid.
58
+ """
59
+ params = {}
60
+ if any(
61
+ [
62
+ red is not None,
63
+ green is not None,
64
+ blue is not None,
65
+ ]
66
+ ):
67
+ params["color"] = (red or 0, green or 0, blue or 0)
68
+
69
+ if margin is not None:
70
+ params["margin"] = int(margin) if margin.is_integer() else margin
71
+ return PdfResponse(
72
+ PdfWrapper(pdf.file.read(), **options.as_kwargs())
73
+ .generate_coordinate_grid(**params)
74
+ .read()
75
+ )
@@ -0,0 +1,127 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module defines web API routes for inspecting PDF form information.
4
+
5
+ It exposes `/inspect` endpoints that return JSON for form schemas, current
6
+ form values, generated sample data, and field rectangle metadata. Each endpoint
7
+ wraps read-only `PdfWrapper` properties for clients calling PyPDFForm over HTTP.
8
+ """
9
+
10
+ from typing import Annotated
11
+
12
+ from fastapi import APIRouter, Depends, File, Form, UploadFile
13
+
14
+ from .. import PdfWrapper
15
+ from ..shared.utils import get_widget
16
+ from .common import (PdfWrapperOptions, api_widget_key_error,
17
+ pdf_wrapper_options)
18
+
19
+ inspect_router = APIRouter(prefix="/inspect", tags=["inspect"])
20
+
21
+
22
+ @inspect_router.post("/schema", summary="Return the form schema as JSON.")
23
+ def schema(
24
+ options: Annotated[PdfWrapperOptions, Depends(pdf_wrapper_options)],
25
+ pdf: Annotated[UploadFile, File()],
26
+ ) -> dict:
27
+ """
28
+ Upload a PDF form and return the JSON schema PyPDFForm detects for its
29
+ fields.
30
+
31
+ Use this response to discover field names, value types, and validation
32
+ constraints before filling or updating a form.
33
+
34
+ \f
35
+
36
+ Args:
37
+ options (PdfWrapperOptions): Common `PdfWrapper` construction options.
38
+ pdf (UploadFile): Uploaded PDF file to inspect.
39
+
40
+ Returns:
41
+ dict: JSON-serializable form schema.
42
+ """
43
+ return PdfWrapper(pdf.file.read(), **options.as_kwargs()).schema
44
+
45
+
46
+ @inspect_router.post("/data", summary="Return current form data as JSON.")
47
+ def data(
48
+ options: Annotated[PdfWrapperOptions, Depends(pdf_wrapper_options)],
49
+ pdf: Annotated[UploadFile, File()],
50
+ ) -> dict:
51
+ """
52
+ Upload a PDF form and return the values currently stored in its fields.
53
+
54
+ Empty fields are included in the response so clients can distinguish
55
+ missing form fields from blank values.
56
+
57
+ \f
58
+
59
+ Args:
60
+ options (PdfWrapperOptions): Common `PdfWrapper` construction options.
61
+ pdf (UploadFile): Uploaded PDF file to inspect.
62
+
63
+ Returns:
64
+ dict: JSON-serializable current form data.
65
+ """
66
+ return PdfWrapper(pdf.file.read(), **options.as_kwargs()).data
67
+
68
+
69
+ @inspect_router.post("/sample", summary="Return sample fill data as JSON.")
70
+ def sample(
71
+ options: Annotated[PdfWrapperOptions, Depends(pdf_wrapper_options)],
72
+ pdf: Annotated[UploadFile, File()],
73
+ ) -> dict:
74
+ """
75
+ Upload a PDF form and return example data matching the detected schema.
76
+
77
+ Use this response as a starting payload when testing form filling or when
78
+ building a client-side editor for a PDF form.
79
+
80
+ \f
81
+
82
+ Args:
83
+ options (PdfWrapperOptions): Common `PdfWrapper` construction options.
84
+ pdf (UploadFile): Uploaded PDF file to inspect.
85
+
86
+ Returns:
87
+ dict: JSON-serializable sample fill data.
88
+ """
89
+ return PdfWrapper(pdf.file.read(), **options.as_kwargs()).sample_data
90
+
91
+
92
+ @inspect_router.post(
93
+ "/location", summary="Return a form field's location and size as JSON."
94
+ )
95
+ def location(
96
+ options: Annotated[PdfWrapperOptions, Depends(pdf_wrapper_options)],
97
+ pdf: Annotated[UploadFile, File()],
98
+ field: Annotated[str, Form()],
99
+ ) -> dict:
100
+ """
101
+ Upload a PDF form and field name, then return that field's page number,
102
+ coordinates, width, and height.
103
+
104
+ Use this endpoint when placing overlays, annotations, or generated content
105
+ relative to an existing form field.
106
+
107
+ \f
108
+
109
+ Args:
110
+ options (PdfWrapperOptions): Common `PdfWrapper` construction options.
111
+ pdf (UploadFile): Uploaded PDF file to inspect.
112
+ field (str): Name of the form field to locate.
113
+
114
+ Returns:
115
+ dict: JSON-serializable field page number, coordinates, and dimensions.
116
+ """
117
+ f = get_widget(
118
+ PdfWrapper(pdf.file.read(), **options.as_kwargs()), field, api_widget_key_error
119
+ )
120
+
121
+ return {
122
+ "page_number": f.page_number,
123
+ "x": f.x,
124
+ "y": f.y,
125
+ "width": f.width,
126
+ "height": f.height,
127
+ }
@@ -0,0 +1,20 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Root router for the PyPDFForm web API.
4
+
5
+ This router owns the top-level API routes that make PyPDFForm available over
6
+ HTTP. Finished endpoint groups should stay aligned with the CLI's behavior so
7
+ users can choose between the Python library, command line, or web API without
8
+ learning a different workflow model.
9
+ """
10
+
11
+ from fastapi import APIRouter
12
+ from fastapi.responses import RedirectResponse
13
+
14
+ root_router = APIRouter()
15
+
16
+
17
+ @root_router.get("/", include_in_schema=False)
18
+ def index() -> RedirectResponse:
19
+ """Redirect the API root to the generated OpenAPI documentation."""
20
+ return RedirectResponse(url="/docs")
@@ -0,0 +1,56 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module defines web API routes for updating existing PDF files.
4
+
5
+ It exposes `/update` endpoints that accept uploaded PDFs, apply matching
6
+ `PdfWrapper` operations, and return modified PDF bytes to HTTP clients.
7
+ """
8
+
9
+ from typing import Annotated
10
+
11
+ from fastapi import APIRouter, Depends, File, Form, UploadFile
12
+
13
+ from .. import PdfWrapper
14
+ from .common import PdfResponse, PdfWrapperOptions, pdf_wrapper_options
15
+
16
+ update_router = APIRouter(prefix="/update", tags=["update"])
17
+
18
+
19
+ @update_router.post(
20
+ "/title",
21
+ summary="Set the PDF title.",
22
+ response_class=PdfResponse,
23
+ responses={
24
+ 200: {
25
+ "content": {
26
+ "application/pdf": {"schema": {"type": "string", "format": "binary"}}
27
+ },
28
+ }
29
+ },
30
+ )
31
+ def title(
32
+ options: Annotated[PdfWrapperOptions, Depends(pdf_wrapper_options)],
33
+ pdf: Annotated[UploadFile, File()],
34
+ new_title: Annotated[str, Form()],
35
+ ) -> PdfResponse:
36
+ """
37
+ Upload a PDF and return a copy with its document title metadata updated.
38
+
39
+ The response body is the modified PDF file, preserving other behavior from
40
+ the common `PdfWrapper` options supplied as query parameters.
41
+
42
+ \f
43
+
44
+ Args:
45
+ options (PdfWrapperOptions): Common `PdfWrapper` construction options.
46
+ pdf (UploadFile): Uploaded PDF file to update.
47
+ new_title (str): New title to write into the PDF metadata.
48
+
49
+ Returns:
50
+ PdfResponse: PDF response containing the updated document bytes.
51
+ """
52
+ return PdfResponse(
53
+ content=PdfWrapper(
54
+ pdf.file.read(), title=new_title, **options.as_kwargs()
55
+ ).read()
56
+ )
@@ -15,7 +15,7 @@ import typer
15
15
  from jsonschema import ValidationError, validate
16
16
 
17
17
  from .. import PdfWrapper
18
- from ..lib.middleware.base import Widget
18
+ from ..shared.utils import WidgetKeyErrorHandler
19
19
 
20
20
  INPUT_PDF = Annotated[
21
21
  Path,
@@ -77,10 +77,10 @@ def json_file_option(help_text: str):
77
77
  )
78
78
 
79
79
 
80
- def _cli_bad_parameter(
80
+ def cli_bad_parameter(
81
81
  message: str,
82
82
  param_hint: str,
83
- cause: BaseException,
83
+ cause: BaseException | None = None,
84
84
  ) -> NoReturn:
85
85
  """
86
86
  Raises a Typer input error with a stable CLI message.
@@ -88,14 +88,35 @@ def _cli_bad_parameter(
88
88
  Args:
89
89
  message (str): Error message to display to the CLI user.
90
90
  param_hint (str): CLI parameter associated with the error.
91
- cause (BaseException): Original exception that caused the CLI error.
91
+ cause (BaseException, optional): Original exception that caused the CLI
92
+ error. Defaults to None.
92
93
 
93
94
  Raises:
94
95
  typer.BadParameter: Raised with the provided message and parameter hint.
95
96
  """
97
+ if cause is None:
98
+ raise typer.BadParameter(message, param_hint=param_hint)
99
+
96
100
  raise typer.BadParameter(message, param_hint=param_hint) from cause
97
101
 
98
102
 
103
+ def cli_widget_key_error(param_hint: str) -> WidgetKeyErrorHandler:
104
+ """
105
+ Build a CLI error handler for missing form fields.
106
+
107
+ Args:
108
+ param_hint (str): CLI parameter associated with the field name.
109
+
110
+ Returns:
111
+ WidgetKeyErrorHandler: Handler that raises a Typer input error.
112
+ """
113
+
114
+ def _raise_cli_bad_parameter(message: str, cause: KeyError) -> NoReturn:
115
+ cli_bad_parameter(message, param_hint=param_hint, cause=cause)
116
+
117
+ return _raise_cli_bad_parameter
118
+
119
+
99
120
  def _validation_error_path(exc: ValidationError) -> str:
100
121
  """
101
122
  Builds a dotted JSON path for a validation error.
@@ -129,7 +150,7 @@ def load_json_file(data: Path, schema: dict, param_hint: str) -> Any:
129
150
  with open(data, "r", encoding="utf-8") as f:
130
151
  input_data = json.load(f)
131
152
  except (OSError, json.JSONDecodeError) as exc:
132
- _cli_bad_parameter(
153
+ cli_bad_parameter(
133
154
  f"Invalid JSON file: {exc}",
134
155
  param_hint=param_hint,
135
156
  cause=exc,
@@ -140,7 +161,7 @@ def load_json_file(data: Path, schema: dict, param_hint: str) -> Any:
140
161
  except ValidationError as exc:
141
162
  error_path = _validation_error_path(exc)
142
163
  location = f" at {error_path}" if error_path else ""
143
- _cli_bad_parameter(
164
+ cli_bad_parameter(
144
165
  f"Invalid JSON file{location}: {exc.message}",
145
166
  param_hint=param_hint,
146
167
  cause=exc,
@@ -149,31 +170,6 @@ def load_json_file(data: Path, schema: dict, param_hint: str) -> Any:
149
170
  return input_data
150
171
 
151
172
 
152
- def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
153
- """
154
- Look up a widget and report missing names as CLI input errors.
155
-
156
- Args:
157
- wrapper (PdfWrapper): PDF wrapper containing form widgets.
158
- field (str): Form field name to look up.
159
- param_hint (str): CLI parameter associated with the field name.
160
-
161
- Returns:
162
- Widget: The matching widget.
163
-
164
- Raises:
165
- typer.BadParameter: Raised when the widget name is not present.
166
- """
167
- try:
168
- return wrapper.widgets[field]
169
- except KeyError as exc:
170
- _cli_bad_parameter(
171
- f"Form field '{field}' does not exist.",
172
- param_hint=param_hint,
173
- cause=exc,
174
- )
175
-
176
-
177
173
  def handle_font_registration(
178
174
  obj: PdfWrapper, params: dict, registered_font: dict
179
175
  ) -> None:
@@ -17,7 +17,8 @@ import typer
17
17
  from .. import (Annotations, BlankPage, Fields, PdfArray, PdfWrapper,
18
18
  RawElements)
19
19
  from .common import (INPUT_PDF, OPTIONAL_OUTPUT_PDF, REQUIRED_OUTPUT_PDF,
20
- create_elements_from_file, json_file_option)
20
+ cli_bad_parameter, create_elements_from_file,
21
+ json_file_option)
21
22
  from .schemas.create import ANNOTATION_SCHEMA, FIELD_SCHEMA, RAW_SCHEMA
22
23
 
23
24
  create_cli = typer.Typer(
@@ -94,6 +95,13 @@ def extract(
94
95
  ] = None,
95
96
  ) -> None:
96
97
  """Extract pages from an existing PDF."""
98
+ if start is not None and end is not None and start > end:
99
+ message = "End page must be greater than or equal to start page."
100
+ cli_bad_parameter(
101
+ message,
102
+ param_hint="--end",
103
+ )
104
+
97
105
  PdfWrapper(str(pdf), **ctx.obj).pages[slice((start or 1) - 1, end)].write(output)
98
106
 
99
107
 
@@ -0,0 +1,37 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Console script entry point for the optional PyPDFForm CLI.
4
+
5
+ The CLI implementation depends on the optional ``cli`` extra. This lightweight
6
+ wrapper lets the ``pypdfform`` command fail with installation guidance instead
7
+ of an import traceback when those optional dependencies are absent.
8
+ """
9
+
10
+ import importlib
11
+ import sys
12
+
13
+ CLI_DEPENDENCIES = {"jsonschema", "typer"}
14
+ CLI_INSTALL_HINT = "pip install 'PyPDFForm[cli]'"
15
+
16
+
17
+ def main() -> None:
18
+ """
19
+ Run the PyPDFForm CLI.
20
+
21
+ Raises:
22
+ SystemExit: Raised with exit code 1 when optional CLI dependencies are
23
+ missing.
24
+ """
25
+ try:
26
+ cli_module = importlib.import_module("PyPDFForm.cli.root")
27
+ except ModuleNotFoundError as exc:
28
+ if exc.name in CLI_DEPENDENCIES:
29
+ print(
30
+ "PyPDFForm CLI dependencies are not installed. "
31
+ f"Install them with: {CLI_INSTALL_HINT}",
32
+ file=sys.stderr,
33
+ )
34
+ raise SystemExit(1) from None
35
+ raise
36
+
37
+ getattr(cli_module, "cli_app")()
@@ -13,7 +13,8 @@ import json
13
13
  import typer
14
14
 
15
15
  from .. import PdfWrapper
16
- from .common import FIELD_NAME, INPUT_PDF, get_widget
16
+ from ..shared.utils import get_widget
17
+ from .common import FIELD_NAME, INPUT_PDF, cli_widget_key_error
17
18
 
18
19
  inspect_cli = typer.Typer(
19
20
  context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
@@ -54,7 +55,9 @@ def location(
54
55
  field: FIELD_NAME,
55
56
  ) -> None:
56
57
  """Print a form field's location and size as JSON."""
57
- f = get_widget(PdfWrapper(str(pdf), **ctx.obj), field, "--field")
58
+ f = get_widget(
59
+ PdfWrapper(str(pdf), **ctx.obj), field, cli_widget_key_error("--field")
60
+ )
58
61
 
59
62
  typer.echo(
60
63
  json.dumps(
@@ -165,6 +165,3 @@ def fill(
165
165
  input_data[k] = input_data[k]["path"]
166
166
 
167
167
  obj.fill(input_data, flatten=flatten).write(output or pdf)
168
-
169
-
170
- __all__ = ["cli_app"]
@@ -1,5 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
- # pylint: disable=R0801
3
2
  """
4
3
  JSON schemas for the PyPDFForm create CLI commands.
5
4
  """
@@ -1,5 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
- # pylint: disable=R0801
3
2
  """
4
3
  JSON schemas for the PyPDFForm update CLI commands.
5
4
  """