Skip to content

Commit 197531b

Browse files
Add Convert Colors in PDF methods
Assisted-by: Codex
1 parent ce5e2de commit 197531b

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
PdfFlattenTransparenciesPayload,
@@ -116,6 +117,7 @@
116117
JpegColorModel,
117118
OcrLanguage,
118119
PdfAType,
120+
PdfColorProfile,
119121
PdfInfoQuery,
120122
PdfMergeInput,
121123
PdfPageSelection,
@@ -2717,6 +2719,41 @@ def compress_pdf(
27172719
timeout=timeout,
27182720
)
27192721

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

3799+
async def convert_colors(
3800+
self,
3801+
file: PdfRestFile | Sequence[PdfRestFile],
3802+
*,
3803+
color_profile: PdfColorProfile,
3804+
profile: PdfRestFile | Sequence[PdfRestFile] | None = None,
3805+
preserve_black: bool = False,
3806+
output: str | None = None,
3807+
extra_query: Query | None = None,
3808+
extra_headers: AnyMapping | None = None,
3809+
extra_body: Body | None = None,
3810+
timeout: TimeoutTypes | None = None,
3811+
) -> PdfRestFileBasedResponse:
3812+
"""Asynchronously convert PDF colors using preset or custom ICC profiles."""
3813+
3814+
payload: dict[str, Any] = {
3815+
"files": file,
3816+
"color_profile": color_profile,
3817+
"preserve_black": preserve_black,
3818+
}
3819+
if profile is not None:
3820+
payload["profile"] = profile
3821+
if output is not None:
3822+
payload["output"] = output
3823+
3824+
return await self._post_file_operation(
3825+
endpoint="/pdf-with-converted-colors",
3826+
payload=payload,
3827+
payload_model=PdfConvertColorsPayload,
3828+
extra_query=extra_query,
3829+
extra_headers=extra_headers,
3830+
extra_body=extra_body,
3831+
timeout=timeout,
3832+
)
3833+
37623834
async def flatten_transparencies(
37633835
self,
37643836
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ..types import (
2626
OcrLanguage,
2727
PdfAType,
28+
PdfColorProfile,
2829
PdfInfoQuery,
2930
PdfXType,
3031
SummaryFormat,
@@ -1142,6 +1143,68 @@ class PdfFlattenAnnotationsPayload(BaseModel):
11421143
] = None
11431144

11441145

1146+
class PdfConvertColorsPayload(BaseModel):
1147+
"""Adapt caller options into a pdfRest-ready convert-colors request payload."""
1148+
1149+
files: Annotated[
1150+
list[PdfRestFile],
1151+
Field(
1152+
min_length=1,
1153+
max_length=1,
1154+
validation_alias=AliasChoices("file", "files"),
1155+
serialization_alias="id",
1156+
),
1157+
BeforeValidator(_ensure_list),
1158+
AfterValidator(
1159+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
1160+
),
1161+
PlainSerializer(_serialize_as_first_file_id),
1162+
]
1163+
color_profile: Annotated[
1164+
PdfColorProfile,
1165+
Field(serialization_alias="color_profile"),
1166+
]
1167+
profile: Annotated[
1168+
list[PdfRestFile] | None,
1169+
Field(
1170+
default=None,
1171+
min_length=1,
1172+
max_length=1,
1173+
validation_alias=AliasChoices("profile", "profiles"),
1174+
serialization_alias="profile_id",
1175+
),
1176+
BeforeValidator(_ensure_list),
1177+
AfterValidator(
1178+
_allowed_mime_types(
1179+
"application/vnd.iccprofile",
1180+
"application/octet-stream",
1181+
error_msg="Profile must be an ICC file",
1182+
)
1183+
),
1184+
PlainSerializer(_serialize_as_first_file_id),
1185+
] = None
1186+
preserve_black: Annotated[
1187+
bool | None,
1188+
Field(serialization_alias="preserve_black", default=None),
1189+
] = None
1190+
output: Annotated[
1191+
str | None,
1192+
Field(serialization_alias="output", min_length=1, default=None),
1193+
AfterValidator(_validate_output_prefix),
1194+
] = None
1195+
1196+
@model_validator(mode="after")
1197+
def _validate_profile_dependency(self) -> PdfConvertColorsPayload:
1198+
if self.color_profile == "custom":
1199+
if not self.profile:
1200+
msg = "color_profile 'custom' requires a profile to be provided."
1201+
raise ValueError(msg)
1202+
elif self.profile:
1203+
msg = "A profile can only be provided when color_profile is 'custom'."
1204+
raise ValueError(msg)
1205+
return self
1206+
1207+
11451208
class BmpPdfRestPayload(BasePdfRestGraphicPayload[Literal["rgb", "gray"]]):
11461209
"""Adapt caller options into a pdfRest-ready BMP request payload."""
11471210

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)