Skip to content

Commit fbfbaa0

Browse files
Add Convert to PDF/X
Assisted-by: Codex
1 parent 9470658 commit fbfbaa0

5 files changed

Lines changed: 239 additions & 1 deletion

File tree

src/pdfrest/client.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
PdfRedactionPreviewPayload,
8080
PdfRestRawFileResponse,
8181
PdfSplitPayload,
82+
PdfToPdfxPayload,
8283
PdfToWordPayload,
8384
PngPdfRestPayload,
8485
TiffPdfRestPayload,
@@ -91,6 +92,7 @@
9192
PdfPageSelection,
9293
PdfRedactionInstruction,
9394
PdfRGBColor,
95+
PdfXType,
9496
)
9597

9698
DEFAULT_BASE_URL = "https://api.pdfrest.com"
@@ -2168,6 +2170,33 @@ def convert_to_word(
21682170
timeout=timeout,
21692171
)
21702172

2173+
def convert_to_pdfx(
2174+
self,
2175+
file: PdfRestFile | Sequence[PdfRestFile],
2176+
*,
2177+
output_type: PdfXType,
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+
"""Convert a PDF to a specified PDF/X version."""
2185+
2186+
payload: dict[str, Any] = {"files": file, "output_type": output_type}
2187+
if output is not None:
2188+
payload["output"] = output
2189+
2190+
return self._post_file_operation(
2191+
endpoint="/pdfx",
2192+
payload=payload,
2193+
payload_model=PdfToPdfxPayload,
2194+
extra_query=extra_query,
2195+
extra_headers=extra_headers,
2196+
extra_body=extra_body,
2197+
timeout=timeout,
2198+
)
2199+
21712200
def convert_to_png(
21722201
self,
21732202
files: PdfRestFile | Sequence[PdfRestFile],
@@ -2625,6 +2654,33 @@ async def convert_to_word(
26252654
timeout=timeout,
26262655
)
26272656

2657+
async def convert_to_pdfx(
2658+
self,
2659+
file: PdfRestFile | Sequence[PdfRestFile],
2660+
*,
2661+
output_type: PdfXType,
2662+
output: str | None = None,
2663+
extra_query: Query | None = None,
2664+
extra_headers: AnyMapping | None = None,
2665+
extra_body: Body | None = None,
2666+
timeout: TimeoutTypes | None = None,
2667+
) -> PdfRestFileBasedResponse:
2668+
"""Asynchronously convert a PDF to a specified PDF/X version."""
2669+
2670+
payload: dict[str, Any] = {"files": file, "output_type": output_type}
2671+
if output is not None:
2672+
payload["output"] = output
2673+
2674+
return await self._post_file_operation(
2675+
endpoint="/pdfx",
2676+
payload=payload,
2677+
payload_model=PdfToPdfxPayload,
2678+
extra_query=extra_query,
2679+
extra_headers=extra_headers,
2680+
extra_body=extra_body,
2681+
timeout=timeout,
2682+
)
2683+
26282684
async def convert_to_png(
26292685
self,
26302686
files: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from pdfrest.types.public import PdfRedactionPreset
2323

24-
from ..types import PdfInfoQuery
24+
from ..types import PdfInfoQuery, PdfXType
2525
from . import PdfRestFile
2626
from .public import PdfRestFileID
2727

@@ -500,6 +500,32 @@ class PdfToWordPayload(BaseModel):
500500
] = None
501501

502502

503+
class PdfToPdfxPayload(BaseModel):
504+
"""Adapt caller options into a pdfRest-ready PDF/X request payload."""
505+
506+
files: Annotated[
507+
list[PdfRestFile],
508+
Field(
509+
min_length=1,
510+
max_length=1,
511+
validation_alias=AliasChoices("file", "files"),
512+
serialization_alias="id",
513+
),
514+
BeforeValidator(_ensure_list),
515+
AfterValidator(
516+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
517+
),
518+
PlainSerializer(_serialize_as_first_file_id),
519+
]
520+
output_type: Annotated[PdfXType, Field(serialization_alias="output_type")]
521+
output: Annotated[
522+
str | None,
523+
Field(serialization_alias="output", min_length=1, default=None),
524+
AfterValidator(_validate_output_prefix),
525+
] = None
526+
527+
528+
503529
class BmpPdfRestPayload(BasePdfRestGraphicPayload[Literal["rgb", "gray"]]):
504530
"""Adapt caller options into a pdfRest-ready BMP request payload."""
505531

src/pdfrest/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
PdfRedactionPreset,
1111
PdfRedactionType,
1212
PdfRGBColor,
13+
PdfXType,
1314
)
1415

1516
__all__ = [
@@ -22,4 +23,5 @@
2223
"PdfRedactionInstruction",
2324
"PdfRedactionPreset",
2425
"PdfRedactionType",
26+
"PdfXType",
2527
]

src/pdfrest/types/public.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"PdfRedactionInstruction",
2323
"PdfRedactionPreset",
2424
"PdfRedactionType",
25+
"PdfXType",
2526
)
2627

2728
PdfInfoQuery = Literal[
@@ -96,3 +97,5 @@ class PdfMergeSource(TypedDict, total=False):
9697

9798

9899
PdfMergeInput = PdfRestFile | PdfMergeSource | tuple[PdfRestFile, PdfPageSelection]
100+
101+
PdfXType = Literal["PDF/X-1a", "PDF/X-3", "PDF/X-4", "PDF/X-6"]

tests/test_convert_to_pdfx.py

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

0 commit comments

Comments
 (0)