Skip to content

Commit 89137df

Browse files
Add Convert Colors in PDF methods
Assisted-by: Codex
1 parent 9feb89a commit 89137df

6 files changed

Lines changed: 608 additions & 0 deletions

File tree

src/pdfrest/client.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
JpegPdfRestPayload,
8383
OcrPdfPayload,
8484
PdfCompressPayload,
85+
PdfConvertColorsPayload,
8586
PdfFlattenAnnotationsPayload,
8687
PdfFlattenFormsPayload,
8788
PdfFlattenLayersPayload,
@@ -116,6 +117,7 @@
116117
GraphicSmoothing,
117118
JpegColorModel,
118119
PdfAType,
120+
PdfColorProfile,
119121
PdfInfoQuery,
120122
PdfMergeInput,
121123
PdfPageSelection,
@@ -2716,6 +2718,41 @@ def compress_pdf(
27162718
timeout=timeout,
27172719
)
27182720

2721+
def convert_colors(
2722+
self,
2723+
file: PdfRestFile | Sequence[PdfRestFile],
2724+
*,
2725+
color_profile: PdfColorProfile,
2726+
profile: PdfRestFile | Sequence[PdfRestFile] | None = None,
2727+
preserve_black: bool = False,
2728+
output: str | None = None,
2729+
extra_query: Query | None = None,
2730+
extra_headers: AnyMapping | None = None,
2731+
extra_body: Body | None = None,
2732+
timeout: TimeoutTypes | None = None,
2733+
) -> PdfRestFileBasedResponse:
2734+
"""Convert PDF colors using preset or custom ICC profiles."""
2735+
2736+
payload: dict[str, Any] = {
2737+
"files": file,
2738+
"color_profile": color_profile,
2739+
"preserve_black": preserve_black,
2740+
}
2741+
if profile is not None:
2742+
payload["profile"] = profile
2743+
if output is not None:
2744+
payload["output"] = output
2745+
2746+
return self._post_file_operation(
2747+
endpoint="/pdf-with-converted-colors",
2748+
payload=payload,
2749+
payload_model=PdfConvertColorsPayload,
2750+
extra_query=extra_query,
2751+
extra_headers=extra_headers,
2752+
extra_body=extra_body,
2753+
timeout=timeout,
2754+
)
2755+
27192756
def flatten_transparencies(
27202757
self,
27212758
file: PdfRestFile | Sequence[PdfRestFile],
@@ -3782,6 +3819,41 @@ async def compress_pdf(
37823819
timeout=timeout,
37833820
)
37843821

3822+
async def convert_colors(
3823+
self,
3824+
file: PdfRestFile | Sequence[PdfRestFile],
3825+
*,
3826+
color_profile: PdfColorProfile,
3827+
profile: PdfRestFile | Sequence[PdfRestFile] | None = None,
3828+
preserve_black: bool = False,
3829+
output: str | None = None,
3830+
extra_query: Query | None = None,
3831+
extra_headers: AnyMapping | None = None,
3832+
extra_body: Body | None = None,
3833+
timeout: TimeoutTypes | None = None,
3834+
) -> PdfRestFileBasedResponse:
3835+
"""Asynchronously convert PDF colors using preset or custom ICC profiles."""
3836+
3837+
payload: dict[str, Any] = {
3838+
"files": file,
3839+
"color_profile": color_profile,
3840+
"preserve_black": preserve_black,
3841+
}
3842+
if profile is not None:
3843+
payload["profile"] = profile
3844+
if output is not None:
3845+
payload["output"] = output
3846+
3847+
return await self._post_file_operation(
3848+
endpoint="/pdf-with-converted-colors",
3849+
payload=payload,
3850+
payload_model=PdfConvertColorsPayload,
3851+
extra_query=extra_query,
3852+
extra_headers=extra_headers,
3853+
extra_body=extra_body,
3854+
timeout=timeout,
3855+
)
3856+
37853857
async def flatten_transparencies(
37863858
self,
37873859
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from ..types import (
2525
PdfAType,
26+
PdfColorProfile,
2627
PdfInfoQuery,
2728
PdfXType,
2829
SummaryFormat,
@@ -1112,6 +1113,68 @@ class PdfFlattenLayersPayload(BaseModel):
11121113
] = None
11131114

11141115

1116+
class PdfConvertColorsPayload(BaseModel):
1117+
"""Adapt caller options into a pdfRest-ready convert-colors request payload."""
1118+
1119+
files: Annotated[
1120+
list[PdfRestFile],
1121+
Field(
1122+
min_length=1,
1123+
max_length=1,
1124+
validation_alias=AliasChoices("file", "files"),
1125+
serialization_alias="id",
1126+
),
1127+
BeforeValidator(_ensure_list),
1128+
AfterValidator(
1129+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
1130+
),
1131+
PlainSerializer(_serialize_as_first_file_id),
1132+
]
1133+
color_profile: Annotated[
1134+
PdfColorProfile,
1135+
Field(serialization_alias="color_profile"),
1136+
]
1137+
profile: Annotated[
1138+
list[PdfRestFile] | None,
1139+
Field(
1140+
default=None,
1141+
min_length=1,
1142+
max_length=1,
1143+
validation_alias=AliasChoices("profile", "profiles"),
1144+
serialization_alias="profile_id",
1145+
),
1146+
BeforeValidator(_ensure_list),
1147+
AfterValidator(
1148+
_allowed_mime_types(
1149+
"application/vnd.iccprofile",
1150+
"application/octet-stream",
1151+
error_msg="Profile must be an ICC file",
1152+
)
1153+
),
1154+
PlainSerializer(_serialize_as_first_file_id),
1155+
] = None
1156+
preserve_black: Annotated[
1157+
bool | None,
1158+
Field(serialization_alias="preserve_black", default=None),
1159+
] = None
1160+
output: Annotated[
1161+
str | None,
1162+
Field(serialization_alias="output", min_length=1, default=None),
1163+
AfterValidator(_validate_output_prefix),
1164+
] = None
1165+
1166+
@model_validator(mode="after")
1167+
def _validate_profile_dependency(self) -> PdfConvertColorsPayload:
1168+
if self.color_profile == "custom":
1169+
if not self.profile:
1170+
msg = "color_profile 'custom' requires a profile to be provided."
1171+
raise ValueError(msg)
1172+
elif self.profile:
1173+
msg = "A profile can only be provided when color_profile is 'custom'."
1174+
raise ValueError(msg)
1175+
return self
1176+
1177+
11151178
class BmpPdfRestPayload(BasePdfRestGraphicPayload[Literal["rgb", "gray"]]):
11161179
"""Adapt caller options into a pdfRest-ready BMP request payload."""
11171180

src/pdfrest/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
GraphicSmoothing,
1111
JpegColorModel,
1212
PdfAType,
13+
PdfColorProfile,
1314
PdfInfoQuery,
1415
PdfMergeInput,
1516
PdfMergeSource,
@@ -37,6 +38,7 @@
3738
"GraphicSmoothing",
3839
"JpegColorModel",
3940
"PdfAType",
41+
"PdfColorProfile",
4042
"PdfInfoQuery",
4143
"PdfMergeInput",
4244
"PdfMergeSource",

src/pdfrest/types/public.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"GraphicSmoothing",
2323
"JpegColorModel",
2424
"PdfAType",
25+
"PdfColorProfile",
2526
"PdfInfoQuery",
2627
"PdfMergeInput",
2728
"PdfMergeSource",
@@ -140,3 +141,21 @@ class PdfMergeSource(TypedDict, total=False):
140141
SummaryOutputType = Literal["json", "file"]
141142

142143
TranslateOutputFormat = Literal["plaintext", "markdown"]
144+
145+
PdfColorProfile = Literal[
146+
"lab-d50",
147+
"srgb",
148+
"apple-rgb",
149+
"color-match-rgb",
150+
"gamma-18",
151+
"gamma-22",
152+
"dot-gain-10",
153+
"dot-gain-15",
154+
"dot-gain-20",
155+
"dot-gain-25",
156+
"dot-gain-30",
157+
"monitor-rgb",
158+
"acrobat5-cmyk",
159+
"acrobat9-cmyk",
160+
"custom",
161+
]
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient
6+
from pdfrest.models import PdfRestFile
7+
8+
from ..resources import get_test_resource_path
9+
10+
11+
@pytest.fixture(scope="module")
12+
def uploaded_pdf_for_color_conversion(
13+
pdfrest_api_key: str,
14+
pdfrest_live_base_url: str,
15+
) -> PdfRestFile:
16+
resource = get_test_resource_path("report.pdf")
17+
with PdfRestClient(
18+
api_key=pdfrest_api_key,
19+
base_url=pdfrest_live_base_url,
20+
) as client:
21+
return client.files.create_from_paths([resource])[0]
22+
23+
24+
@pytest.mark.parametrize(
25+
"output_name",
26+
[
27+
pytest.param(None, id="default-output"),
28+
pytest.param("converted-colors", id="custom-output"),
29+
],
30+
)
31+
def test_live_convert_colors_success(
32+
pdfrest_api_key: str,
33+
pdfrest_live_base_url: str,
34+
uploaded_pdf_for_color_conversion: PdfRestFile,
35+
output_name: str | None,
36+
) -> None:
37+
kwargs: dict[str, str | bool] = {"color_profile": "srgb"}
38+
if output_name is not None:
39+
kwargs["output"] = output_name
40+
41+
with PdfRestClient(
42+
api_key=pdfrest_api_key,
43+
base_url=pdfrest_live_base_url,
44+
) as client:
45+
response = client.convert_colors(uploaded_pdf_for_color_conversion, **kwargs)
46+
47+
assert response.output_files
48+
output_file = response.output_file
49+
assert output_file.type == "application/pdf"
50+
assert output_file.size > 0
51+
assert response.warning is None
52+
assert str(response.input_id) == str(uploaded_pdf_for_color_conversion.id)
53+
if output_name is not None:
54+
assert output_file.name.startswith(output_name)
55+
else:
56+
assert output_file.name.endswith(".pdf")
57+
58+
59+
@pytest.mark.asyncio
60+
async def test_live_async_convert_colors_success(
61+
pdfrest_api_key: str,
62+
pdfrest_live_base_url: str,
63+
uploaded_pdf_for_color_conversion: PdfRestFile,
64+
) -> None:
65+
async with AsyncPdfRestClient(
66+
api_key=pdfrest_api_key,
67+
base_url=pdfrest_live_base_url,
68+
) as client:
69+
response = await client.convert_colors(
70+
uploaded_pdf_for_color_conversion,
71+
color_profile="srgb",
72+
output="async",
73+
)
74+
75+
assert response.output_files
76+
output_file = response.output_file
77+
assert output_file.name.startswith("async")
78+
assert output_file.type == "application/pdf"
79+
assert output_file.size > 0
80+
assert response.warning is None
81+
assert str(response.input_id) == str(uploaded_pdf_for_color_conversion.id)
82+
83+
84+
def test_live_convert_colors_invalid_file_id(
85+
pdfrest_api_key: str,
86+
pdfrest_live_base_url: str,
87+
uploaded_pdf_for_color_conversion: PdfRestFile,
88+
) -> None:
89+
with (
90+
PdfRestClient(
91+
api_key=pdfrest_api_key,
92+
base_url=pdfrest_live_base_url,
93+
) as client,
94+
pytest.raises(PdfRestApiError, match=r"(?i)(id|file)"),
95+
):
96+
client.convert_colors(
97+
uploaded_pdf_for_color_conversion,
98+
color_profile="srgb",
99+
extra_body={"id": "00000000-0000-0000-0000-000000000000"},
100+
)
101+
102+
103+
@pytest.mark.asyncio
104+
async def test_live_async_convert_colors_invalid_file_id(
105+
pdfrest_api_key: str,
106+
pdfrest_live_base_url: str,
107+
uploaded_pdf_for_color_conversion: PdfRestFile,
108+
) -> None:
109+
async with AsyncPdfRestClient(
110+
api_key=pdfrest_api_key,
111+
base_url=pdfrest_live_base_url,
112+
) as client:
113+
with pytest.raises(PdfRestApiError, match=r"(?i)(id|file)"):
114+
await client.convert_colors(
115+
uploaded_pdf_for_color_conversion,
116+
color_profile="srgb",
117+
extra_body={"id": "ffffffff-ffff-ffff-ffff-ffffffffffff"},
118+
)

0 commit comments

Comments
 (0)