PyPDFForm 4.8.3__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 (93) hide show
  1. {pypdfform-4.8.3 → pypdfform-4.8.4}/PKG-INFO +11 -9
  2. {pypdfform-4.8.3 → 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.3 → pypdfform-4.8.4}/PyPDFForm/cli/common.py +18 -26
  10. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/inspect.py +5 -2
  11. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/schemas/create.py +0 -1
  12. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/schemas/update.py +0 -1
  13. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/update.py +7 -5
  14. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/raw/image.py +0 -1
  15. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/raw/rect.py +0 -1
  16. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/dropdown.py +0 -1
  17. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/signature.py +0 -1
  18. pypdfform-4.8.4/PyPDFForm/shared/__init__.py +0 -0
  19. pypdfform-4.8.4/PyPDFForm/shared/utils.py +42 -0
  20. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm.egg-info/PKG-INFO +11 -9
  21. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm.egg-info/SOURCES.txt +8 -0
  22. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm.egg-info/requires.txt +11 -8
  23. {pypdfform-4.8.3 → pypdfform-4.8.4}/pyproject.toml +12 -9
  24. {pypdfform-4.8.3 → pypdfform-4.8.4}/LICENSE +0 -0
  25. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/__init__.py +0 -0
  26. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/create.py +0 -0
  27. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/entry.py +0 -0
  28. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/root.py +0 -0
  29. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/cli/schemas/__init__.py +0 -0
  30. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/__init__.py +0 -0
  31. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/adapter.py +0 -0
  32. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/__init__.py +0 -0
  33. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/base.py +0 -0
  34. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/link.py +0 -0
  35. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/stamp.py +0 -0
  36. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/text.py +0 -0
  37. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  38. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/assets/__init__.py +0 -0
  39. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/assets/bedrock.py +0 -0
  40. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/assets/blank.py +0 -0
  41. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/constants.py +0 -0
  42. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/coordinate.py +0 -0
  43. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/deprecation.py +0 -0
  44. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/egress.py +0 -0
  45. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/filler.py +0 -0
  46. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/font.py +0 -0
  47. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/hooks.py +0 -0
  48. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/image.py +0 -0
  49. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/__init__.py +0 -0
  50. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/base.py +0 -0
  51. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  52. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  53. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/image.py +0 -0
  54. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/radio.py +0 -0
  55. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/signature.py +0 -0
  56. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/text.py +0 -0
  57. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/patterns.py +0 -0
  58. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/raw/__init__.py +0 -0
  59. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/raw/circle.py +0 -0
  60. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/raw/ellipse.py +0 -0
  61. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/raw/line.py +0 -0
  62. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/raw/text.py +0 -0
  63. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/template.py +0 -0
  64. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/types.py +0 -0
  65. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/utils.py +0 -0
  66. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/watermark.py +0 -0
  67. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/__init__.py +0 -0
  68. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/base.py +0 -0
  69. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  70. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/image.py +0 -0
  71. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/radio.py +0 -0
  72. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/text.py +0 -0
  73. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm/lib/wrapper.py +0 -0
  74. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  75. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm.egg-info/entry_points.txt +0 -0
  76. {pypdfform-4.8.3 → pypdfform-4.8.4}/PyPDFForm.egg-info/top_level.txt +0 -0
  77. {pypdfform-4.8.3 → pypdfform-4.8.4}/README.md +0 -0
  78. {pypdfform-4.8.3 → pypdfform-4.8.4}/setup.cfg +0 -0
  79. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_bulk_create_fields.py +0 -0
  80. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_create_widget.py +0 -0
  81. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_draw_elements.py +0 -0
  82. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_dropdown.py +0 -0
  83. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_extract_middleware_attributes.py +0 -0
  84. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_fill_max_length_text_field.py +0 -0
  85. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_font_widths.py +0 -0
  86. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_functional.py +0 -0
  87. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_generate_appearance_streams.py +0 -0
  88. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_js.py +0 -0
  89. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_need_appearances.py +0 -0
  90. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_paragraph.py +0 -0
  91. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_signature.py +0 -0
  92. {pypdfform-4.8.3 → pypdfform-4.8.4}/tests/test_use_full_widget_name.py +0 -0
  93. {pypdfform-4.8.3 → 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.3
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,27 +20,29 @@ 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<48.0.0,>=47.0.0
23
+ Requires-Dist: cryptography<49.0.0,>=48.0.0
24
24
  Requires-Dist: fonttools<5.0.0,>=4.62.1
25
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.25.0; 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.13.5; 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"
45
+ Requires-Dist: requests<3.0.0,>=2.34.0; extra == "dev"
44
46
  Requires-Dist: ruff<1.0.0,>=0.15.12; extra == "dev"
45
47
  Dynamic: license-file
46
48
 
@@ -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.3"
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,
@@ -100,6 +100,23 @@ def cli_bad_parameter(
100
100
  raise typer.BadParameter(message, param_hint=param_hint) from cause
101
101
 
102
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
+
103
120
  def _validation_error_path(exc: ValidationError) -> str:
104
121
  """
105
122
  Builds a dotted JSON path for a validation error.
@@ -153,31 +170,6 @@ def load_json_file(data: Path, schema: dict, param_hint: str) -> Any:
153
170
  return input_data
154
171
 
155
172
 
156
- def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
157
- """
158
- Look up a widget and report missing names as CLI input errors.
159
-
160
- Args:
161
- wrapper (PdfWrapper): PDF wrapper containing form widgets.
162
- field (str): Form field name to look up.
163
- param_hint (str): CLI parameter associated with the field name.
164
-
165
- Returns:
166
- Widget: The matching widget.
167
-
168
- Raises:
169
- typer.BadParameter: Raised when the widget name is not present.
170
- """
171
- try:
172
- return wrapper.widgets[field]
173
- except KeyError as exc:
174
- cli_bad_parameter(
175
- f"Form field '{field}' does not exist.",
176
- param_hint=param_hint,
177
- cause=exc,
178
- )
179
-
180
-
181
173
  def handle_font_registration(
182
174
  obj: PdfWrapper, params: dict, registered_font: dict
183
175
  ) -> None:
@@ -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(
@@ -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
  """
@@ -17,9 +17,11 @@ import typer
17
17
 
18
18
  from .. import PdfWrapper
19
19
  from ..lib.constants import PdfVersion
20
+ from ..shared.utils import get_widget
20
21
  from .common import (FIELD_NAME, INPUT_PDF, OPTIONAL_OUTPUT_PDF,
21
- cli_bad_parameter, get_widget, handle_font_registration,
22
- json_file_option, load_json_file)
22
+ cli_bad_parameter, cli_widget_key_error,
23
+ handle_font_registration, json_file_option,
24
+ load_json_file)
23
25
  from .schemas.update import FIELD_SCHEMA, RENAME_SCHEMA
24
26
 
25
27
  update_cli = typer.Typer(
@@ -107,7 +109,7 @@ def bounds(
107
109
  ) -> None:
108
110
  """Update a form field's position and size."""
109
111
  obj = PdfWrapper(str(pdf), **ctx.obj)
110
- f = get_widget(obj, widget, "--field")
112
+ f = get_widget(obj, widget, cli_widget_key_error("--field"))
111
113
 
112
114
  f.x = x if x is not None else f.x
113
115
  f.y = y if y is not None else f.y
@@ -137,7 +139,7 @@ def rename(
137
139
  obj = PdfWrapper(str(pdf), **ctx.obj)
138
140
  for item in input_data:
139
141
  for k, v in item.items():
140
- widget = get_widget(obj, k, "--file")
142
+ widget = get_widget(obj, k, cli_widget_key_error("--file"))
141
143
  obj.update_widget_key(
142
144
  widget.name, v["new_key"], index=v.get("index", 0), defer=True
143
145
  )
@@ -160,7 +162,7 @@ def field(
160
162
  obj = PdfWrapper(str(pdf), **ctx.obj)
161
163
  registered_font = {}
162
164
  for k, each in input_data.items():
163
- widget = get_widget(obj, k, "--file")
165
+ widget = get_widget(obj, k, cli_widget_key_error("--file"))
164
166
  handle_font_registration(obj, each, registered_font)
165
167
  for param, v in each.items():
166
168
  setattr(widget, param, v)
@@ -1,5 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
- # pylint: disable=R0801
3
2
  """
4
3
  Contains the RawImage class, which represents an image that can be drawn
5
4
  directly onto a PDF page at a specified position and size.
@@ -1,5 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
- # pylint: disable=R0801
3
2
  """
4
3
  Contains the RawRectangle class, which represents a rectangle that can be drawn
5
4
  directly onto a PDF page at specified coordinates and dimensions.
@@ -89,7 +89,6 @@ class DropdownField(Field):
89
89
  options: Optional[List[str | Tuple[str, str]]] = None
90
90
  width: Optional[float] = None
91
91
  height: Optional[float] = None
92
- # pylint: disable=R0801
93
92
  font: Optional[str] = None
94
93
  font_size: Optional[float] = None
95
94
  font_color: Optional[Tuple[float, ...]] = None
@@ -121,7 +121,6 @@ class SignatureWidget:
121
121
  annot_type_to_annot[key] = annot.get_object()
122
122
 
123
123
  for i, p in enumerate(input_pdf.pages):
124
- # pylint: disable=R0801
125
124
  watermark = BytesIO()
126
125
  canvas = Canvas(
127
126
  watermark,
File without changes
@@ -0,0 +1,42 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Shared helpers used by multiple PyPDFForm interfaces.
4
+
5
+ These utilities contain behavior that should stay consistent across the CLI,
6
+ web API, and any future user-facing entry points.
7
+ """
8
+
9
+ from collections.abc import Callable
10
+ from typing import NoReturn
11
+
12
+ from .. import PdfWrapper
13
+ from ..lib.middleware.base import Widget
14
+
15
+ WidgetKeyErrorHandler = Callable[[str, KeyError], NoReturn]
16
+
17
+
18
+ def get_widget(
19
+ wrapper: PdfWrapper,
20
+ field: str,
21
+ key_error_handler: WidgetKeyErrorHandler,
22
+ ) -> Widget:
23
+ """
24
+ Look up a widget by field name.
25
+
26
+ Args:
27
+ wrapper (PdfWrapper): PDF wrapper containing form widgets.
28
+ field (str): Form field name to look up.
29
+ key_error_handler (WidgetKeyErrorHandler, optional): Interface-specific
30
+ error handler for missing field names. Defaults to None.
31
+
32
+ Returns:
33
+ Widget: The matching widget.
34
+
35
+ Raises:
36
+ KeyError: Raised when the widget name is not present and no handler is
37
+ supplied.
38
+ """
39
+ try:
40
+ return wrapper.widgets[field]
41
+ except KeyError as exc:
42
+ return key_error_handler(f"Form field '{field}' does not exist.", exc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.8.3
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,27 +20,29 @@ 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<48.0.0,>=47.0.0
23
+ Requires-Dist: cryptography<49.0.0,>=48.0.0
24
24
  Requires-Dist: fonttools<5.0.0,>=4.62.1
25
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.25.0; 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.13.5; 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"
45
+ Requires-Dist: requests<3.0.0,>=2.34.0; extra == "dev"
44
46
  Requires-Dist: ruff<1.0.0,>=0.15.12; extra == "dev"
45
47
  Dynamic: license-file
46
48
 
@@ -8,6 +8,12 @@ PyPDFForm.egg-info/dependency_links.txt
8
8
  PyPDFForm.egg-info/entry_points.txt
9
9
  PyPDFForm.egg-info/requires.txt
10
10
  PyPDFForm.egg-info/top_level.txt
11
+ PyPDFForm/api/__init__.py
12
+ PyPDFForm/api/common.py
13
+ PyPDFForm/api/create.py
14
+ PyPDFForm/api/inspect.py
15
+ PyPDFForm/api/root.py
16
+ PyPDFForm/api/update.py
11
17
  PyPDFForm/cli/__init__.py
12
18
  PyPDFForm/cli/common.py
13
19
  PyPDFForm/cli/create.py
@@ -66,6 +72,8 @@ PyPDFForm/lib/widgets/image.py
66
72
  PyPDFForm/lib/widgets/radio.py
67
73
  PyPDFForm/lib/widgets/signature.py
68
74
  PyPDFForm/lib/widgets/text.py
75
+ PyPDFForm/shared/__init__.py
76
+ PyPDFForm/shared/utils.py
69
77
  tests/test_bulk_create_fields.py
70
78
  tests/test_create_widget.py
71
79
  tests/test_draw_elements.py
@@ -1,24 +1,27 @@
1
- cryptography<48.0.0,>=47.0.0
1
+ cryptography<49.0.0,>=48.0.0
2
2
  fonttools<5.0.0,>=4.62.1
3
3
  pikepdf<11.0.0,>=10.5.1
4
4
  pillow<13.0.0,>=12.2.0
5
- pypdf<7.0.0,>=6.10.1
6
- reportlab<5.0.0,>=4.4.6
5
+ pypdf<7.0.0,>=6.11.0
6
+ reportlab<5.0.0,>=4.5.0
7
7
 
8
8
  [cli]
9
- typer<1.0.0,>=0.25.0
9
+ typer<1.0.0,>=0.25.1
10
10
  jsonschema<5.0.0,>=4.26.0
11
11
 
12
12
  [dev]
13
13
  black<27.0.0,>=26.3.1
14
- coverage<8.0.0,>=7.13.5
14
+ coverage<8.0.0,>=7.14.0
15
15
  isort<9.0.0,>=8.0.1
16
- mike<3.0.0,>=2.1.3
16
+ mike<3.0.0,>=2.2.0
17
17
  mkdocs<2.0.0,>=1.6.1
18
18
  mkdocs-material<10.0.0,>=9.7.6
19
19
  pudb<2026.0.0,>=2025.1.5
20
20
  pylint<5.0.0,>=4.0.5
21
- pyright<2.0.0,>=1.1.407
21
+ pyright<2.0.0,>=1.1.409
22
22
  pytest<10.0.0,>=9.0.3
23
- requests<3.0.0,>=2.33.1
23
+ requests<3.0.0,>=2.34.0
24
24
  ruff<1.0.0,>=0.15.12
25
+
26
+ [web_api]
27
+ fastapi[standard]<1.0.0,>=0.136.1
@@ -25,12 +25,12 @@ classifiers = [
25
25
  ]
26
26
  requires-python = ">=3.10"
27
27
  dependencies = [
28
- "cryptography>=47.0.0,<48.0.0",
28
+ "cryptography>=48.0.0,<49.0.0",
29
29
  "fonttools>=4.62.1,<5.0.0",
30
30
  "pikepdf>=10.5.1,<11.0.0",
31
31
  "pillow>=12.2.0,<13.0.0",
32
- "pypdf>=6.10.1,<7.0.0",
33
- "reportlab>=4.4.6,<5.0.0",
32
+ "pypdf>=6.11.0,<7.0.0",
33
+ "reportlab>=4.5.0,<5.0.0",
34
34
  ]
35
35
 
36
36
  [project.urls]
@@ -38,19 +38,20 @@ Homepage = "https://github.com/chinapandaman/PyPDFForm"
38
38
  Documentation = "https://chinapandaman.github.io/PyPDFForm/"
39
39
 
40
40
  [project.optional-dependencies]
41
- cli = ["typer>=0.25.0,<1.0.0", "jsonschema>=4.26.0,<5.0.0"]
41
+ cli = ["typer>=0.25.1,<1.0.0", "jsonschema>=4.26.0,<5.0.0"]
42
+ web_api = ["fastapi[standard]>=0.136.1,<1.0.0"]
42
43
  dev = [
43
44
  "black>=26.3.1,<27.0.0",
44
- "coverage>=7.13.5,<8.0.0",
45
+ "coverage>=7.14.0,<8.0.0",
45
46
  "isort>=8.0.1,<9.0.0",
46
- "mike>=2.1.3,<3.0.0",
47
+ "mike>=2.2.0,<3.0.0",
47
48
  "mkdocs>=1.6.1,<2.0.0",
48
49
  "mkdocs-material>=9.7.6,<10.0.0",
49
50
  "pudb>=2025.1.5,<2026.0.0",
50
51
  "pylint>=4.0.5,<5.0.0",
51
- "pyright>=1.1.407,<2.0.0",
52
+ "pyright>=1.1.409,<2.0.0",
52
53
  "pytest>=9.0.3,<10.0.0",
53
- "requests>=2.33.1,<3.0.0",
54
+ "requests>=2.34.0,<3.0.0",
54
55
  "ruff>=0.15.12,<1.0.0",
55
56
  ]
56
57
 
@@ -73,6 +74,7 @@ disable = [
73
74
  "C2801",
74
75
  "C0301",
75
76
  "W0511",
77
+ "R0801",
76
78
  ]
77
79
 
78
80
  [tool.ruff.lint]
@@ -137,6 +139,7 @@ include = ["PyPDFForm*"]
137
139
 
138
140
  [tool.pytest.ini_options]
139
141
  markers = [
140
- "posix_only", # mainly because of zlib vs zlib-ng
142
+ "posix_only", # mainly because of zlib vs zlib-ng
141
143
  "cli_test",
144
+ "web_api_test",
142
145
  ]
File without changes
File without changes
File without changes
File without changes