Skip to content

Commit 531efe7

Browse files
Merge pull request #15 from datalogics-cgreen/pdfcloud-5464-convert-colors
PDFCLOUD-5550 Add client methods to convert colors in PDF
2 parents 63203d6 + ccafaf3 commit 531efe7

7 files changed

Lines changed: 987 additions & 0 deletions

File tree

src/pdfrest/client.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
PdfAddTextPayload,
9494
PdfBlankPayload,
9595
PdfCompressPayload,
96+
PdfConvertColorsPayload,
9697
PdfDecryptPayload,
9798
PdfEncryptPayload,
9899
PdfExportFormDataPayload,
@@ -149,6 +150,7 @@
149150
PdfPageOrientation,
150151
PdfPageSelection,
151152
PdfPageSize,
153+
PdfPresetColorProfile,
152154
PdfRedactionInstruction,
153155
PdfRestriction,
154156
PdfRGBColor,
@@ -3238,6 +3240,38 @@ def blank_pdf(
32383240
timeout=timeout,
32393241
)
32403242

3243+
def convert_colors(
3244+
self,
3245+
file: PdfRestFile | Sequence[PdfRestFile],
3246+
*,
3247+
color_profile: PdfPresetColorProfile | PdfRestFile | Sequence[PdfRestFile],
3248+
preserve_black: bool = False,
3249+
output: str | None = None,
3250+
extra_query: Query | None = None,
3251+
extra_headers: AnyMapping | None = None,
3252+
extra_body: Body | None = None,
3253+
timeout: TimeoutTypes | None = None,
3254+
) -> PdfRestFileBasedResponse:
3255+
"""Convert PDF colors using presets or a custom uploaded ICC profile."""
3256+
3257+
payload: dict[str, Any] = {
3258+
"files": file,
3259+
"color_profile": color_profile,
3260+
"preserve_black": preserve_black,
3261+
}
3262+
if output is not None:
3263+
payload["output"] = output
3264+
3265+
return self._post_file_operation(
3266+
endpoint="/pdf-with-converted-colors",
3267+
payload=payload,
3268+
payload_model=PdfConvertColorsPayload,
3269+
extra_query=extra_query,
3270+
extra_headers=extra_headers,
3271+
extra_body=extra_body,
3272+
timeout=timeout,
3273+
)
3274+
32413275
def flatten_transparencies(
32423276
self,
32433277
file: PdfRestFile | Sequence[PdfRestFile],
@@ -5003,6 +5037,38 @@ async def blank_pdf(
50035037
timeout=timeout,
50045038
)
50055039

5040+
async def convert_colors(
5041+
self,
5042+
file: PdfRestFile | Sequence[PdfRestFile],
5043+
*,
5044+
color_profile: PdfPresetColorProfile | PdfRestFile | Sequence[PdfRestFile],
5045+
preserve_black: bool = False,
5046+
output: str | None = None,
5047+
extra_query: Query | None = None,
5048+
extra_headers: AnyMapping | None = None,
5049+
extra_body: Body | None = None,
5050+
timeout: TimeoutTypes | None = None,
5051+
) -> PdfRestFileBasedResponse:
5052+
"""Asynchronously convert PDF colors using presets or a custom ICC profile."""
5053+
5054+
payload: dict[str, Any] = {
5055+
"files": file,
5056+
"color_profile": color_profile,
5057+
"preserve_black": preserve_black,
5058+
}
5059+
if output is not None:
5060+
payload["output"] = output
5061+
5062+
return await self._post_file_operation(
5063+
endpoint="/pdf-with-converted-colors",
5064+
payload=payload,
5065+
payload_model=PdfConvertColorsPayload,
5066+
extra_query=extra_query,
5067+
extra_headers=extra_headers,
5068+
extra_body=extra_body,
5069+
timeout=timeout,
5070+
)
5071+
50065072
async def flatten_transparencies(
50075073
self,
50085074
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
PdfInfoQuery,
3636
PdfPageOrientation,
3737
PdfPageSize,
38+
PdfPresetColorProfile,
3839
PdfRestriction,
3940
PdfXType,
4041
SummaryFormat,
@@ -44,6 +45,8 @@
4445
)
4546
from .public import PdfRestFile, PdfRestFileID
4647

48+
PdfConvertColorProfile = PdfPresetColorProfile | Literal["custom"]
49+
4750

4851
def _ensure_list(value: Any) -> Any:
4952
if value is None:
@@ -55,6 +58,14 @@ def _ensure_list(value: Any) -> Any:
5558
return [value]
5659

5760

61+
def _is_uploaded_file_value(value: Any) -> bool:
62+
if isinstance(value, PdfRestFile):
63+
return True
64+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
65+
return all(isinstance(item, PdfRestFile) for item in value)
66+
return False
67+
68+
5869
def _list_of_strings(value: list[Any]) -> list[str]:
5970
return [str(e) for e in value]
6071

@@ -145,6 +156,12 @@ def _bool_to_on_off(value: Any) -> Any:
145156
return value
146157

147158

159+
def _bool_to_true_false(value: Any) -> Any:
160+
if isinstance(value, bool):
161+
return "true" if value else "false"
162+
return value
163+
164+
148165
def _serialize_page_ranges(value: list[str | int | tuple[str | int, ...]]) -> str:
149166
def join_tuple(value: str | int | tuple[str | int, ...]) -> str:
150167
if isinstance(value, tuple):
@@ -1855,6 +1872,96 @@ def _validate_page_configuration(self) -> PdfBlankPayload:
18551872
return self
18561873

18571874

1875+
class PdfConvertColorsPayload(BaseModel):
1876+
"""Adapt caller options into a pdfRest-ready convert-colors request payload."""
1877+
1878+
files: Annotated[
1879+
list[PdfRestFile],
1880+
Field(
1881+
min_length=1,
1882+
max_length=1,
1883+
validation_alias=AliasChoices("file", "files"),
1884+
serialization_alias="id",
1885+
),
1886+
BeforeValidator(_ensure_list),
1887+
AfterValidator(
1888+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
1889+
),
1890+
PlainSerializer(_serialize_as_first_file_id),
1891+
]
1892+
color_profile: Annotated[
1893+
PdfConvertColorProfile,
1894+
Field(serialization_alias="color_profile"),
1895+
]
1896+
custom_profile: Annotated[
1897+
list[PdfRestFile] | None,
1898+
Field(
1899+
default=None,
1900+
min_length=1,
1901+
max_length=1,
1902+
serialization_alias="profile_id",
1903+
),
1904+
BeforeValidator(_ensure_list),
1905+
AfterValidator(
1906+
_allowed_mime_types(
1907+
"application/vnd.iccprofile",
1908+
"application/octet-stream",
1909+
error_msg="Profile must be an ICC file",
1910+
)
1911+
),
1912+
PlainSerializer(_serialize_as_first_file_id),
1913+
] = None
1914+
preserve_black: Annotated[
1915+
Literal["true", "false"] | None,
1916+
Field(serialization_alias="preserve_black", default=None),
1917+
BeforeValidator(_bool_to_true_false),
1918+
] = None
1919+
output: Annotated[
1920+
str | None,
1921+
Field(serialization_alias="output", min_length=1, default=None),
1922+
AfterValidator(_validate_output_prefix),
1923+
] = None
1924+
1925+
@model_validator(mode="before")
1926+
@classmethod
1927+
def _normalize_color_profile(cls, value: Any) -> Any:
1928+
if not isinstance(value, dict):
1929+
return value
1930+
1931+
payload = cast(dict[str, Any], value).copy()
1932+
color_profile = payload.get("color_profile")
1933+
if not _is_uploaded_file_value(color_profile):
1934+
return payload
1935+
1936+
if payload.get("custom_profile") is not None:
1937+
msg = (
1938+
"Provide the custom profile file via color_profile only when "
1939+
"color_profile is a file."
1940+
)
1941+
raise ValueError(msg)
1942+
1943+
payload["color_profile"] = "custom"
1944+
payload["custom_profile"] = color_profile
1945+
return payload
1946+
1947+
@model_validator(mode="after")
1948+
def _validate_profile_dependency(self) -> PdfConvertColorsPayload:
1949+
if self.color_profile == "custom":
1950+
if not self.custom_profile:
1951+
msg = (
1952+
"A custom color profile requires an uploaded ICC file passed "
1953+
"as color_profile."
1954+
)
1955+
raise ValueError(msg)
1956+
elif self.custom_profile:
1957+
msg = (
1958+
"A profile can only be provided by passing an uploaded ICC file "
1959+
"as color_profile."
1960+
)
1961+
raise ValueError(msg)
1962+
return self
1963+
1964+
18581965
class PdfRestrictPayload(BaseModel):
18591966
"""Adapt caller options into a pdfRest-ready restrict-PDF request payload."""
18601967

src/pdfrest/types/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
PdfAddTextObject,
2020
PdfAType,
2121
PdfCmykColor,
22+
PdfColorProfile,
2223
PdfConversionCompression,
2324
PdfConversionDownsample,
2425
PdfConversionLocale,
@@ -29,6 +30,7 @@
2930
PdfPageOrientation,
3031
PdfPageSelection,
3132
PdfPageSize,
33+
PdfPresetColorProfile,
3234
PdfRedactionInstruction,
3335
PdfRedactionPreset,
3436
PdfRedactionType,
@@ -62,6 +64,7 @@
6264
"PdfAType",
6365
"PdfAddTextObject",
6466
"PdfCmykColor",
67+
"PdfColorProfile",
6568
"PdfConversionCompression",
6669
"PdfConversionDownsample",
6770
"PdfConversionLocale",
@@ -72,6 +75,7 @@
7275
"PdfPageOrientation",
7376
"PdfPageSelection",
7477
"PdfPageSize",
78+
"PdfPresetColorProfile",
7579
"PdfRGBColor",
7680
"PdfRedactionInstruction",
7781
"PdfRedactionPreset",

src/pdfrest/types/public.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"PdfAType",
3232
"PdfAddTextObject",
3333
"PdfCmykColor",
34+
"PdfColorProfile",
3435
"PdfConversionCompression",
3536
"PdfConversionDownsample",
3637
"PdfConversionLocale",
@@ -41,6 +42,7 @@
4142
"PdfPageOrientation",
4243
"PdfPageSelection",
4344
"PdfPageSize",
45+
"PdfPresetColorProfile",
4446
"PdfRGBColor",
4547
"PdfRedactionInstruction",
4648
"PdfRedactionPreset",
@@ -223,3 +225,21 @@ class PdfMergeSource(TypedDict, total=False):
223225
)
224226
PdfPageSize = Literal["letter", "legal", "ledger", "A3", "A4", "A5"] | PdfCustomPageSize
225227
PdfPageOrientation = Literal["portrait", "landscape"]
228+
PdfPresetColorProfile = Literal[
229+
"lab-d50",
230+
"srgb",
231+
"apple-rgb",
232+
"color-match-rgb",
233+
"gamma-18",
234+
"gamma-22",
235+
"dot-gain-10",
236+
"dot-gain-15",
237+
"dot-gain-20",
238+
"dot-gain-25",
239+
"dot-gain-30",
240+
"monitor-rgb",
241+
"acrobat5-cmyk",
242+
"acrobat9-cmyk",
243+
]
244+
245+
PdfColorProfile = PdfPresetColorProfile

0 commit comments

Comments
 (0)