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.
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PKG-INFO +13 -11
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/__init__.py +1 -1
- pypdfform-4.8.4/PyPDFForm/api/__init__.py +28 -0
- pypdfform-4.8.4/PyPDFForm/api/common.py +101 -0
- pypdfform-4.8.4/PyPDFForm/api/create.py +75 -0
- pypdfform-4.8.4/PyPDFForm/api/inspect.py +127 -0
- pypdfform-4.8.4/PyPDFForm/api/root.py +20 -0
- pypdfform-4.8.4/PyPDFForm/api/update.py +56 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/common.py +27 -31
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/create.py +9 -1
- pypdfform-4.8.4/PyPDFForm/cli/entry.py +37 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/inspect.py +5 -2
- pypdfform-4.8.2/PyPDFForm/cli/__init__.py → pypdfform-4.8.4/PyPDFForm/cli/root.py +0 -3
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/schemas/create.py +0 -1
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/schemas/update.py +0 -1
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/cli/update.py +13 -4
- pypdfform-4.8.4/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/image.py +0 -1
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/rect.py +0 -1
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/dropdown.py +0 -1
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/signature.py +0 -1
- pypdfform-4.8.4/PyPDFForm/shared/__init__.py +0 -0
- pypdfform-4.8.4/PyPDFForm/shared/utils.py +42 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm.egg-info/PKG-INFO +13 -11
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm.egg-info/SOURCES.txt +10 -0
- pypdfform-4.8.4/PyPDFForm.egg-info/entry_points.txt +2 -0
- pypdfform-4.8.4/PyPDFForm.egg-info/requires.txt +27 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/pyproject.toml +15 -12
- pypdfform-4.8.2/PyPDFForm.egg-info/entry_points.txt +0 -2
- pypdfform-4.8.2/PyPDFForm.egg-info/requires.txt +0 -24
- {pypdfform-4.8.2 → pypdfform-4.8.4}/LICENSE +0 -0
- {pypdfform-4.8.2/PyPDFForm/cli/schemas → pypdfform-4.8.4/PyPDFForm/cli}/__init__.py +0 -0
- {pypdfform-4.8.2/PyPDFForm/lib → pypdfform-4.8.4/PyPDFForm/cli/schemas}/__init__.py +0 -0
- {pypdfform-4.8.2/PyPDFForm/lib/assets → pypdfform-4.8.4/PyPDFForm/lib}/__init__.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/egress.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/font.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/patterns.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/template.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/watermark.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/base.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm/lib/wrapper.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/README.md +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/setup.cfg +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_create_widget.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_draw_elements.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_dropdown.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_font_widths.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_functional.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_js.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_need_appearances.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_paragraph.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_signature.py +0 -0
- {pypdfform-4.8.2 → pypdfform-4.8.4}/tests/test_use_full_widget_name.py +0 -0
- {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.
|
|
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<
|
|
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.
|
|
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.
|
|
28
|
-
Requires-Dist: reportlab<5.0.0,>=4.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
44
|
-
Requires-Dist: ruff<1.0.0,>=0.
|
|
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>
|
|
@@ -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 ..
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 .
|
|
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(
|
|
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(
|