Skip to content

Commit 83656f8

Browse files
Add Convert Colors in PDF methods
Assisted-by: Codex
1 parent ed9ee65 commit 83656f8

6 files changed

Lines changed: 607 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,
@@ -117,6 +118,7 @@
117118
JpegColorModel,
118119
OcrLanguage,
119120
PdfAType,
121+
PdfColorProfile,
120122
PdfInfoQuery,
121123
PdfMergeInput,
122124
PdfPageSelection,
@@ -2718,6 +2720,41 @@ def compress_pdf(
27182720
timeout=timeout,
27192721
)
27202722

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

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

src/pdfrest/models/_internal.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from ..types import (
2525
OcrLanguage,
2626
PdfAType,
27+
PdfColorProfile,
2728
PdfInfoQuery,
2829
PdfXType,
2930
SummaryFormat,
@@ -1125,6 +1126,68 @@ class PdfFlattenLayersPayload(BaseModel):
11251126
] = None
11261127

11271128

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

src/pdfrest/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
JpegColorModel,
1313
OcrLanguage,
1414
PdfAType,
15+
PdfColorProfile,
1516
PdfInfoQuery,
1617
PdfMergeInput,
1718
PdfMergeSource,
@@ -41,6 +42,7 @@
4142
"JpegColorModel",
4243
"OcrLanguage",
4344
"PdfAType",
45+
"PdfColorProfile",
4446
"PdfInfoQuery",
4547
"PdfMergeInput",
4648
"PdfMergeSource",

src/pdfrest/types/public.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"JpegColorModel",
2525
"OcrLanguage",
2626
"PdfAType",
27+
"PdfColorProfile",
2728
"PdfInfoQuery",
2829
"PdfMergeInput",
2930
"PdfMergeSource",
@@ -160,3 +161,20 @@ class PdfMergeSource(TypedDict, total=False):
160161
ALL_OCR_LANGUAGES: tuple[OcrLanguage, ...] = cast(
161162
tuple[OcrLanguage, ...], get_args(OcrLanguage)
162163
)
164+
PdfColorProfile = Literal[
165+
"lab-d50",
166+
"srgb",
167+
"apple-rgb",
168+
"color-match-rgb",
169+
"gamma-18",
170+
"gamma-22",
171+
"dot-gain-10",
172+
"dot-gain-15",
173+
"dot-gain-20",
174+
"dot-gain-25",
175+
"dot-gain-30",
176+
"monitor-rgb",
177+
"acrobat5-cmyk",
178+
"acrobat9-cmyk",
179+
"custom",
180+
]
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)