Skip to content

Commit b01cb78

Browse files
Add Blank PDF methods
- Add `blank_pdf()` sync and async methods - Ensure compatibility with Blank PDF response: - Allow PdfRestRawFileResponse.input_id to default empty so missing inputId doesn’t fail validation - When normalizing file responses, fall back to raw ids (outputId) when inputId is absent for blank-pdf - Document blank-pdf handling to keep response construction working without server-provided input ids
1 parent 89137df commit b01cb78

6 files changed

Lines changed: 566 additions & 3 deletions

File tree

src/pdfrest/client.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
GifPdfRestPayload,
8282
JpegPdfRestPayload,
8383
OcrPdfPayload,
84+
PdfBlankPayload,
8485
PdfCompressPayload,
8586
PdfConvertColorsPayload,
8687
PdfFlattenAnnotationsPayload,
@@ -120,7 +121,9 @@
120121
PdfColorProfile,
121122
PdfInfoQuery,
122123
PdfMergeInput,
124+
PdfPageOrientation,
123125
PdfPageSelection,
126+
PdfPageSize,
124127
PdfRedactionInstruction,
125128
PdfRGBColor,
126129
PdfXType,
@@ -1031,8 +1034,9 @@ def _post_file_operation(
10311034
for file_id in output_ids
10321035
]
10331036

1037+
input_ids = raw_response.input_id or (raw_response.ids or [])
10341038
response_payload: dict[str, Any] = {
1035-
"input_id": [str(file_id) for file_id in raw_response.input_id],
1039+
"input_id": [str(file_id) for file_id in input_ids],
10361040
"output_file": [
10371041
file.model_dump(mode="json", by_alias=True) for file in output_files
10381042
],
@@ -1306,8 +1310,9 @@ async def throttled_fetch_file_info(file_id: str) -> PdfRestFile:
13061310
)
13071311
)
13081312

1313+
input_ids = raw_response.input_id or (raw_response.ids or [])
13091314
response_payload: dict[str, Any] = {
1310-
"input_id": [str(file_id) for file_id in raw_response.input_id],
1315+
"input_id": [str(file_id) for file_id in input_ids],
13111316
"output_file": [
13121317
file.model_dump(mode="json", by_alias=True) for file in output_files
13131318
],
@@ -2753,6 +2758,45 @@ def convert_colors(
27532758
timeout=timeout,
27542759
)
27552760

2761+
def blank_pdf(
2762+
self,
2763+
*,
2764+
page_size: PdfPageSize,
2765+
page_count: int,
2766+
page_orientation: PdfPageOrientation | None = None,
2767+
custom_height: float | None = None,
2768+
custom_width: float | None = None,
2769+
output: str | None = None,
2770+
extra_query: Query | None = None,
2771+
extra_headers: AnyMapping | None = None,
2772+
extra_body: Body | None = None,
2773+
timeout: TimeoutTypes | None = None,
2774+
) -> PdfRestFileBasedResponse:
2775+
"""Create a blank PDF with the specified size, count, and orientation."""
2776+
2777+
payload: dict[str, Any] = {
2778+
"page_size": page_size,
2779+
"page_count": page_count,
2780+
}
2781+
if page_orientation is not None:
2782+
payload["page_orientation"] = page_orientation
2783+
if custom_height is not None:
2784+
payload["custom_height"] = custom_height
2785+
if custom_width is not None:
2786+
payload["custom_width"] = custom_width
2787+
if output is not None:
2788+
payload["output"] = output
2789+
2790+
return self._post_file_operation(
2791+
endpoint="/blank-pdf",
2792+
payload=payload,
2793+
payload_model=PdfBlankPayload,
2794+
extra_query=extra_query,
2795+
extra_headers=extra_headers,
2796+
extra_body=extra_body,
2797+
timeout=timeout,
2798+
)
2799+
27562800
def flatten_transparencies(
27572801
self,
27582802
file: PdfRestFile | Sequence[PdfRestFile],
@@ -3854,6 +3898,45 @@ async def convert_colors(
38543898
timeout=timeout,
38553899
)
38563900

3901+
async def blank_pdf(
3902+
self,
3903+
*,
3904+
page_size: PdfPageSize,
3905+
page_count: int,
3906+
page_orientation: PdfPageOrientation | None = None,
3907+
custom_height: float | None = None,
3908+
custom_width: float | None = None,
3909+
output: str | None = None,
3910+
extra_query: Query | None = None,
3911+
extra_headers: AnyMapping | None = None,
3912+
extra_body: Body | None = None,
3913+
timeout: TimeoutTypes | None = None,
3914+
) -> PdfRestFileBasedResponse:
3915+
"""Asynchronously create a blank PDF with the specified size."""
3916+
3917+
payload: dict[str, Any] = {
3918+
"page_size": page_size,
3919+
"page_count": page_count,
3920+
}
3921+
if page_orientation is not None:
3922+
payload["page_orientation"] = page_orientation
3923+
if custom_height is not None:
3924+
payload["custom_height"] = custom_height
3925+
if custom_width is not None:
3926+
payload["custom_width"] = custom_width
3927+
if output is not None:
3928+
payload["output"] = output
3929+
3930+
return await self._post_file_operation(
3931+
endpoint="/blank-pdf",
3932+
payload=payload,
3933+
payload_model=PdfBlankPayload,
3934+
extra_query=extra_query,
3935+
extra_headers=extra_headers,
3936+
extra_body=extra_body,
3937+
timeout=timeout,
3938+
)
3939+
38573940
async def flatten_transparencies(
38583941
self,
38593942
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
PdfAType,
2626
PdfColorProfile,
2727
PdfInfoQuery,
28+
PdfPageOrientation,
29+
PdfPageSize,
2830
PdfXType,
2931
SummaryFormat,
3032
SummaryOutputFormat,
@@ -1113,6 +1115,57 @@ class PdfFlattenLayersPayload(BaseModel):
11131115
] = None
11141116

11151117

1118+
class PdfBlankPayload(BaseModel):
1119+
"""Adapt caller options into a pdfRest-ready blank PDF request payload."""
1120+
1121+
page_size: Annotated[
1122+
PdfPageSize,
1123+
Field(serialization_alias="page_size"),
1124+
]
1125+
page_count: Annotated[
1126+
int,
1127+
Field(serialization_alias="page_count", ge=1, le=1000),
1128+
]
1129+
page_orientation: Annotated[
1130+
PdfPageOrientation | None,
1131+
Field(serialization_alias="page_orientation", default=None),
1132+
] = None
1133+
custom_height: Annotated[
1134+
float | None,
1135+
Field(serialization_alias="custom_height", gt=0, default=None),
1136+
] = None
1137+
custom_width: Annotated[
1138+
float | None,
1139+
Field(serialization_alias="custom_width", gt=0, default=None),
1140+
] = None
1141+
output: Annotated[
1142+
str | None,
1143+
Field(serialization_alias="output", min_length=1, default=None),
1144+
AfterValidator(_validate_output_prefix),
1145+
] = None
1146+
1147+
@model_validator(mode="after")
1148+
def _validate_page_configuration(self) -> PdfBlankPayload:
1149+
is_custom = self.page_size == "custom"
1150+
has_custom_height = self.custom_height is not None
1151+
has_custom_width = self.custom_width is not None
1152+
if is_custom:
1153+
if not (has_custom_height and has_custom_width):
1154+
msg = "custom_height and custom_width are required when page_size is 'custom'."
1155+
raise ValueError(msg)
1156+
if self.page_orientation is not None:
1157+
msg = "page_orientation must be omitted when page_size is 'custom'."
1158+
raise ValueError(msg)
1159+
else:
1160+
if self.page_orientation is None:
1161+
msg = "page_orientation is required when page_size is not 'custom'."
1162+
raise ValueError(msg)
1163+
if has_custom_height or has_custom_width:
1164+
msg = "custom_height and custom_width can only be provided when page_size is 'custom'."
1165+
raise ValueError(msg)
1166+
return self
1167+
1168+
11161169
class PdfConvertColorsPayload(BaseModel):
11171170
"""Adapt caller options into a pdfRest-ready convert-colors request payload."""
11181171

@@ -1226,7 +1279,11 @@ class PdfRestRawFileResponse(BaseModel):
12261279

12271280
input_id: Annotated[
12281281
list[PdfRestFileID],
1229-
Field(alias="inputId", description="The id of the input file"),
1282+
Field(
1283+
alias="inputId",
1284+
description="The id of the input file",
1285+
default_factory=list,
1286+
),
12301287
BeforeValidator(_ensure_list),
12311288
]
12321289
output_urls: Annotated[

src/pdfrest/types/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
PdfInfoQuery,
1515
PdfMergeInput,
1616
PdfMergeSource,
17+
PdfPageOrientation,
1718
PdfPageSelection,
19+
PdfPageSize,
1820
PdfRedactionInstruction,
1921
PdfRedactionPreset,
2022
PdfRedactionType,
@@ -42,7 +44,9 @@
4244
"PdfInfoQuery",
4345
"PdfMergeInput",
4446
"PdfMergeSource",
47+
"PdfPageOrientation",
4548
"PdfPageSelection",
49+
"PdfPageSize",
4650
"PdfRGBColor",
4751
"PdfRedactionInstruction",
4852
"PdfRedactionPreset",

src/pdfrest/types/public.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
"PdfInfoQuery",
2727
"PdfMergeInput",
2828
"PdfMergeSource",
29+
"PdfPageOrientation",
2930
"PdfPageSelection",
31+
"PdfPageSize",
3032
"PdfRGBColor",
3133
"PdfRedactionInstruction",
3234
"PdfRedactionPreset",
@@ -159,3 +161,6 @@ class PdfMergeSource(TypedDict, total=False):
159161
"acrobat9-cmyk",
160162
"custom",
161163
]
164+
165+
PdfPageSize = Literal["letter", "legal", "ledger", "A3", "A4", "A5", "custom"]
166+
PdfPageOrientation = Literal["portrait", "landscape"]

tests/live/test_live_blank_pdf.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient
6+
7+
8+
@pytest.mark.parametrize(
9+
"output_name",
10+
[
11+
pytest.param(None, id="default-output"),
12+
pytest.param("blank-doc", id="custom-output"),
13+
],
14+
)
15+
def test_live_blank_pdf_success(
16+
pdfrest_api_key: str,
17+
pdfrest_live_base_url: str,
18+
output_name: str | None,
19+
) -> None:
20+
kwargs: dict[str, str | int] = {
21+
"page_size": "letter",
22+
"page_count": 1,
23+
"page_orientation": "portrait",
24+
}
25+
if output_name is not None:
26+
kwargs["output"] = output_name
27+
28+
with PdfRestClient(
29+
api_key=pdfrest_api_key,
30+
base_url=pdfrest_live_base_url,
31+
) as client:
32+
response = client.blank_pdf(**kwargs)
33+
34+
assert response.output_files
35+
output_file = response.output_file
36+
assert output_file.type == "application/pdf"
37+
assert output_file.size > 0
38+
assert response.warning is None
39+
if output_name is not None:
40+
assert output_file.name.startswith(output_name)
41+
else:
42+
assert output_file.name.endswith(".pdf")
43+
44+
45+
@pytest.mark.asyncio
46+
async def test_live_async_blank_pdf_success(
47+
pdfrest_api_key: str,
48+
pdfrest_live_base_url: str,
49+
) -> None:
50+
async with AsyncPdfRestClient(
51+
api_key=pdfrest_api_key,
52+
base_url=pdfrest_live_base_url,
53+
) as client:
54+
response = await client.blank_pdf(
55+
page_size="A4",
56+
page_count=2,
57+
page_orientation="landscape",
58+
output="async-blank",
59+
)
60+
61+
assert response.output_files
62+
output_file = response.output_file
63+
assert output_file.name.startswith("async-blank")
64+
assert output_file.type == "application/pdf"
65+
assert output_file.size > 0
66+
assert response.warning is None
67+
68+
69+
def test_live_blank_pdf_invalid_request(
70+
pdfrest_api_key: str,
71+
pdfrest_live_base_url: str,
72+
) -> None:
73+
with (
74+
PdfRestClient(
75+
api_key=pdfrest_api_key,
76+
base_url=pdfrest_live_base_url,
77+
) as client,
78+
pytest.raises(PdfRestApiError, match=r"(?i)(page|size)"),
79+
):
80+
client.blank_pdf(
81+
page_size="letter",
82+
page_count=1,
83+
page_orientation="portrait",
84+
extra_body={"page_size": "not-a-size"},
85+
)
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_live_async_blank_pdf_invalid_request(
90+
pdfrest_api_key: str,
91+
pdfrest_live_base_url: str,
92+
) -> None:
93+
async with AsyncPdfRestClient(
94+
api_key=pdfrest_api_key,
95+
base_url=pdfrest_live_base_url,
96+
) as client:
97+
with pytest.raises(PdfRestApiError, match=r"(?i)(page|size)"):
98+
await client.blank_pdf(
99+
page_size="letter",
100+
page_count=1,
101+
page_orientation="portrait",
102+
extra_body={"page_size": "bad-size"},
103+
)

0 commit comments

Comments
 (0)