Skip to content

Commit 8f1051c

Browse files
Add Flatten Forms tool
Assisted-by: Codex
1 parent fbfbaa0 commit 8f1051c

3 files changed

Lines changed: 219 additions & 0 deletions

File tree

src/pdfrest/client.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
PdfRedactionPreviewPayload,
8080
PdfRestRawFileResponse,
8181
PdfSplitPayload,
82+
PdfFlattenFormsPayload,
8283
PdfToPdfxPayload,
8384
PdfToWordPayload,
8485
PngPdfRestPayload,
@@ -2170,6 +2171,32 @@ def convert_to_word(
21702171
timeout=timeout,
21712172
)
21722173

2174+
def flatten_pdf_forms(
2175+
self,
2176+
file: PdfRestFile | Sequence[PdfRestFile],
2177+
*,
2178+
output: str | None = None,
2179+
extra_query: Query | None = None,
2180+
extra_headers: AnyMapping | None = None,
2181+
extra_body: Body | None = None,
2182+
timeout: TimeoutTypes | None = None,
2183+
) -> PdfRestFileBasedResponse:
2184+
"""Flatten form fields in a PDF so they are no longer editable."""
2185+
2186+
payload: dict[str, Any] = {"files": file}
2187+
if output is not None:
2188+
payload["output"] = output
2189+
2190+
return self._post_file_operation(
2191+
endpoint="/flattened-forms-pdf",
2192+
payload=payload,
2193+
payload_model=PdfFlattenFormsPayload,
2194+
extra_query=extra_query,
2195+
extra_headers=extra_headers,
2196+
extra_body=extra_body,
2197+
timeout=timeout,
2198+
)
2199+
21732200
def convert_to_pdfx(
21742201
self,
21752202
file: PdfRestFile | Sequence[PdfRestFile],
@@ -2654,6 +2681,32 @@ async def convert_to_word(
26542681
timeout=timeout,
26552682
)
26562683

2684+
async def flatten_pdf_forms(
2685+
self,
2686+
file: PdfRestFile | Sequence[PdfRestFile],
2687+
*,
2688+
output: str | None = None,
2689+
extra_query: Query | None = None,
2690+
extra_headers: AnyMapping | None = None,
2691+
extra_body: Body | None = None,
2692+
timeout: TimeoutTypes | None = None,
2693+
) -> PdfRestFileBasedResponse:
2694+
"""Asynchronously flatten form fields in a PDF."""
2695+
2696+
payload: dict[str, Any] = {"files": file}
2697+
if output is not None:
2698+
payload["output"] = output
2699+
2700+
return await self._post_file_operation(
2701+
endpoint="/flattened-forms-pdf",
2702+
payload=payload,
2703+
payload_model=PdfFlattenFormsPayload,
2704+
extra_query=extra_query,
2705+
extra_headers=extra_headers,
2706+
extra_body=extra_body,
2707+
timeout=timeout,
2708+
)
2709+
26572710
async def convert_to_pdfx(
26582711
self,
26592712
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,29 @@ class PdfToPdfxPayload(BaseModel):
525525
] = None
526526

527527

528+
class PdfFlattenFormsPayload(BaseModel):
529+
"""Adapt caller options into a pdfRest-ready flatten-forms request payload."""
530+
531+
files: Annotated[
532+
list[PdfRestFile],
533+
Field(
534+
min_length=1,
535+
max_length=1,
536+
validation_alias=AliasChoices("file", "files"),
537+
serialization_alias="id",
538+
),
539+
BeforeValidator(_ensure_list),
540+
AfterValidator(
541+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
542+
),
543+
PlainSerializer(_serialize_as_first_file_id),
544+
]
545+
output: Annotated[
546+
str | None,
547+
Field(serialization_alias="output", min_length=1, default=None),
548+
AfterValidator(_validate_output_prefix),
549+
] = None
550+
528551

529552
class BmpPdfRestPayload(BasePdfRestGraphicPayload[Literal["rgb", "gray"]]):
530553
"""Adapt caller options into a pdfRest-ready BMP request payload."""

tests/test_flatten_pdf_forms.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from __future__ import annotations
2+
3+
import json
4+
5+
import httpx
6+
import pytest
7+
from pydantic import ValidationError
8+
9+
from pdfrest import AsyncPdfRestClient, PdfRestClient
10+
from pdfrest.models import PdfRestFile, PdfRestFileBasedResponse, PdfRestFileID
11+
from pdfrest.models._internal import PdfFlattenFormsPayload
12+
13+
from .graphics_test_helpers import (
14+
ASYNC_API_KEY,
15+
VALID_API_KEY,
16+
build_file_info_payload,
17+
make_pdf_file,
18+
)
19+
20+
21+
def test_flatten_pdf_forms_success(monkeypatch: pytest.MonkeyPatch) -> None:
22+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
23+
input_file = make_pdf_file(PdfRestFileID.generate(1))
24+
output_id = str(PdfRestFileID.generate())
25+
26+
payload_dump = PdfFlattenFormsPayload.model_validate(
27+
{"files": [input_file], "output": "flattened"}
28+
).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True)
29+
30+
seen: dict[str, int] = {"post": 0, "get": 0}
31+
32+
def handler(request: httpx.Request) -> httpx.Response:
33+
if request.method == "POST" and request.url.path == "/flattened-forms-pdf":
34+
seen["post"] += 1
35+
payload = json.loads(request.content.decode("utf-8"))
36+
assert payload == payload_dump
37+
return httpx.Response(
38+
200,
39+
json={
40+
"inputId": [input_file.id],
41+
"outputId": [output_id],
42+
},
43+
)
44+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
45+
seen["get"] += 1
46+
assert request.url.params["format"] == "info"
47+
return httpx.Response(
48+
200,
49+
json=build_file_info_payload(
50+
output_id,
51+
"flattened.pdf",
52+
"application/pdf",
53+
),
54+
)
55+
msg = f"Unexpected request {request.method} {request.url}"
56+
raise AssertionError(msg)
57+
58+
transport = httpx.MockTransport(handler)
59+
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
60+
response = client.flatten_pdf_forms(input_file, output="flattened")
61+
62+
assert seen == {"post": 1, "get": 1}
63+
assert isinstance(response, PdfRestFileBasedResponse)
64+
assert response.output_file.name == "flattened.pdf"
65+
assert response.output_file.type == "application/pdf"
66+
assert str(response.input_id) == str(input_file.id)
67+
assert response.warning is None
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_async_flatten_pdf_forms_success(monkeypatch: pytest.MonkeyPatch) -> None:
72+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
73+
input_file = make_pdf_file(PdfRestFileID.generate(2))
74+
output_id = str(PdfRestFileID.generate())
75+
76+
payload_dump = PdfFlattenFormsPayload.model_validate(
77+
{"files": [input_file]}
78+
).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True)
79+
80+
seen: dict[str, int] = {"post": 0, "get": 0}
81+
82+
def handler(request: httpx.Request) -> httpx.Response:
83+
if request.method == "POST" and request.url.path == "/flattened-forms-pdf":
84+
seen["post"] += 1
85+
payload = json.loads(request.content.decode("utf-8"))
86+
assert payload == payload_dump
87+
return httpx.Response(
88+
200,
89+
json={
90+
"inputId": [input_file.id],
91+
"outputId": [output_id],
92+
},
93+
)
94+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
95+
seen["get"] += 1
96+
assert request.url.params["format"] == "info"
97+
return httpx.Response(
98+
200,
99+
json=build_file_info_payload(
100+
output_id,
101+
"async.pdf",
102+
"application/pdf",
103+
),
104+
)
105+
msg = f"Unexpected request {request.method} {request.url}"
106+
raise AssertionError(msg)
107+
108+
transport = httpx.MockTransport(handler)
109+
async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client:
110+
response = await client.flatten_pdf_forms(input_file)
111+
112+
assert seen == {"post": 1, "get": 1}
113+
assert isinstance(response, PdfRestFileBasedResponse)
114+
assert response.output_file.name == "async.pdf"
115+
assert response.output_file.type == "application/pdf"
116+
assert str(response.input_id) == str(input_file.id)
117+
118+
119+
def test_flatten_pdf_forms_validation(monkeypatch: pytest.MonkeyPatch) -> None:
120+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
121+
pdf_file = make_pdf_file(PdfRestFileID.generate(1))
122+
png_file = PdfRestFile.model_validate(
123+
build_file_info_payload(
124+
PdfRestFileID.generate(),
125+
"example.png",
126+
"image/png",
127+
)
128+
)
129+
transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError))
130+
131+
with (
132+
PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client,
133+
pytest.raises(ValidationError, match="Must be a PDF file"),
134+
):
135+
client.flatten_pdf_forms(png_file)
136+
137+
with (
138+
PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client,
139+
pytest.raises(
140+
ValidationError, match="List should have at most 1 item after validation"
141+
),
142+
):
143+
client.flatten_pdf_forms([pdf_file, make_pdf_file(PdfRestFileID.generate())])

0 commit comments

Comments
 (0)