Skip to content

Commit 399b49a

Browse files
Add Watermark PDF tool
Assisted-by: Codex
1 parent ce5e2de commit 399b49a

4 files changed

Lines changed: 239 additions & 0 deletions

File tree

src/pdfrest/client.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
PdfToPdfxPayload,
9999
PdfToPowerpointPayload,
100100
PdfToWordPayload,
101+
PdfWatermarkPayload,
101102
PdfXfaToAcroformsPayload,
102103
PngPdfRestPayload,
103104
SummarizePdfTextPayload,
@@ -116,6 +117,7 @@
116117
JpegColorModel,
117118
OcrLanguage,
118119
PdfAType,
120+
PdfCMYKColor,
119121
PdfInfoQuery,
120122
PdfMergeInput,
121123
PdfPageSelection,
@@ -127,6 +129,8 @@
127129
SummaryOutputFormat,
128130
TiffColorModel,
129131
TranslateOutputFormat,
132+
WatermarkHorizontalAlignment,
133+
WatermarkVerticalAlignment,
130134
)
131135

132136
__all__ = ("AsyncPdfRestClient", "PdfRestClient")
@@ -2822,6 +2826,65 @@ def rasterize_pdf(
28222826
timeout=timeout,
28232827
)
28242828

2829+
def watermark_pdf(
2830+
self,
2831+
file: PdfRestFile | Sequence[PdfRestFile],
2832+
*,
2833+
watermark_text: str | None = None,
2834+
watermark_file: PdfRestFile | Sequence[PdfRestFile] | None = None,
2835+
output: str | None = None,
2836+
font: str | None = None,
2837+
text_size: int = 72,
2838+
text_color_rgb: PdfRGBColor | Sequence[int] | None = None,
2839+
text_color_cmyk: PdfCMYKColor | Sequence[int] | None = None,
2840+
watermark_file_scale: float = 0.5,
2841+
opacity: float = 0.5,
2842+
horizontal_alignment: WatermarkHorizontalAlignment = "center",
2843+
vertical_alignment: WatermarkVerticalAlignment = "center",
2844+
x: int = 0,
2845+
y: int = 0,
2846+
rotation: int = 0,
2847+
pages: PdfPageSelection | None = None,
2848+
behind_page: bool = False,
2849+
extra_query: Query | None = None,
2850+
extra_headers: AnyMapping | None = None,
2851+
extra_body: Body | None = None,
2852+
timeout: TimeoutTypes | None = None,
2853+
) -> PdfRestFileBasedResponse:
2854+
"""Apply a text or file watermark to a PDF."""
2855+
2856+
payload: dict[str, Any] = {
2857+
"files": file,
2858+
"watermark_text": watermark_text,
2859+
"watermark_file": watermark_file,
2860+
"font": font,
2861+
"text_size": text_size,
2862+
"text_color_rgb": text_color_rgb,
2863+
"text_color_cmyk": text_color_cmyk,
2864+
"watermark_file_scale": watermark_file_scale,
2865+
"opacity": opacity,
2866+
"horizontal_alignment": horizontal_alignment,
2867+
"vertical_alignment": vertical_alignment,
2868+
"x": x,
2869+
"y": y,
2870+
"rotation": rotation,
2871+
"behind_page": behind_page,
2872+
}
2873+
if output is not None:
2874+
payload["output"] = output
2875+
if pages is not None:
2876+
payload["pages"] = pages
2877+
2878+
return self._post_file_operation(
2879+
endpoint="/watermarked-pdf",
2880+
payload=payload,
2881+
payload_model=PdfWatermarkPayload,
2882+
extra_query=extra_query,
2883+
extra_headers=extra_headers,
2884+
extra_body=extra_body,
2885+
timeout=timeout,
2886+
)
2887+
28252888
def convert_to_pdfa(
28262889
self,
28272890
file: PdfRestFile | Sequence[PdfRestFile],
@@ -3864,6 +3927,65 @@ async def rasterize_pdf(
38643927
timeout=timeout,
38653928
)
38663929

3930+
async def watermark_pdf(
3931+
self,
3932+
file: PdfRestFile | Sequence[PdfRestFile],
3933+
*,
3934+
watermark_text: str | None = None,
3935+
watermark_file: PdfRestFile | Sequence[PdfRestFile] | None = None,
3936+
output: str | None = None,
3937+
font: str | None = None,
3938+
text_size: int = 72,
3939+
text_color_rgb: PdfRGBColor | Sequence[int] | None = None,
3940+
text_color_cmyk: PdfCMYKColor | Sequence[int] | None = None,
3941+
watermark_file_scale: float = 0.5,
3942+
opacity: float = 0.5,
3943+
horizontal_alignment: WatermarkHorizontalAlignment = "center",
3944+
vertical_alignment: WatermarkVerticalAlignment = "center",
3945+
x: int = 0,
3946+
y: int = 0,
3947+
rotation: int = 0,
3948+
pages: PdfPageSelection | None = None,
3949+
behind_page: bool = False,
3950+
extra_query: Query | None = None,
3951+
extra_headers: AnyMapping | None = None,
3952+
extra_body: Body | None = None,
3953+
timeout: TimeoutTypes | None = None,
3954+
) -> PdfRestFileBasedResponse:
3955+
"""Asynchronously apply a text or file watermark to a PDF."""
3956+
3957+
payload: dict[str, Any] = {
3958+
"files": file,
3959+
"watermark_text": watermark_text,
3960+
"watermark_file": watermark_file,
3961+
"font": font,
3962+
"text_size": text_size,
3963+
"text_color_rgb": text_color_rgb,
3964+
"text_color_cmyk": text_color_cmyk,
3965+
"watermark_file_scale": watermark_file_scale,
3966+
"opacity": opacity,
3967+
"horizontal_alignment": horizontal_alignment,
3968+
"vertical_alignment": vertical_alignment,
3969+
"x": x,
3970+
"y": y,
3971+
"rotation": rotation,
3972+
"behind_page": behind_page,
3973+
}
3974+
if output is not None:
3975+
payload["output"] = output
3976+
if pages is not None:
3977+
payload["pages"] = pages
3978+
3979+
return await self._post_file_operation(
3980+
endpoint="/watermarked-pdf",
3981+
payload=payload,
3982+
payload_model=PdfWatermarkPayload,
3983+
extra_query=extra_query,
3984+
extra_headers=extra_headers,
3985+
extra_body=extra_body,
3986+
timeout=timeout,
3987+
)
3988+
38673989
async def convert_to_pdfa(
38683990
self,
38693991
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
SummaryOutputFormat,
3232
SummaryOutputType,
3333
TranslateOutputFormat,
34+
WatermarkHorizontalAlignment,
35+
WatermarkVerticalAlignment,
3436
)
3537
from . import PdfRestFile
3638
from .public import PdfRestFileID
@@ -563,6 +565,7 @@ class ExtractImagesPayload(BaseModel):
563565

564566

565567
RgbChannel = Annotated[int, Field(ge=0, le=255)]
568+
CmykChannel = Annotated[int, Field(ge=0, le=100)]
566569

567570

568571
class PdfLiteralRedactionModel(BaseModel):
@@ -1021,6 +1024,107 @@ def _validate_profile_dependency(self) -> PdfCompressPayload:
10211024
return self
10221025

10231026

1027+
class PdfWatermarkPayload(BaseModel):
1028+
"""Adapt caller options into a pdfRest-ready watermark request payload."""
1029+
1030+
files: Annotated[
1031+
list[PdfRestFile],
1032+
Field(
1033+
min_length=1,
1034+
max_length=1,
1035+
validation_alias=AliasChoices("file", "files"),
1036+
serialization_alias="id",
1037+
),
1038+
BeforeValidator(_ensure_list),
1039+
AfterValidator(
1040+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
1041+
),
1042+
PlainSerializer(_serialize_as_first_file_id),
1043+
]
1044+
watermark_text: Annotated[
1045+
str | None,
1046+
Field(serialization_alias="watermark_text", min_length=1, default=None),
1047+
] = None
1048+
watermark_file: Annotated[
1049+
list[PdfRestFile] | None,
1050+
Field(
1051+
default=None,
1052+
min_length=1,
1053+
max_length=1,
1054+
serialization_alias="watermark_file_id",
1055+
),
1056+
BeforeValidator(_ensure_list),
1057+
AfterValidator(
1058+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
1059+
),
1060+
PlainSerializer(_serialize_as_first_file_id),
1061+
] = None
1062+
output: Annotated[
1063+
str | None,
1064+
Field(serialization_alias="output", min_length=1, default=None),
1065+
AfterValidator(_validate_output_prefix),
1066+
] = None
1067+
font: Annotated[
1068+
str | None, Field(serialization_alias="font", min_length=1, default=None)
1069+
] = None
1070+
text_size: Annotated[
1071+
int,
1072+
Field(serialization_alias="text_size", ge=5, le=100, default=72),
1073+
] = 72
1074+
text_color_rgb: Annotated[
1075+
tuple[RgbChannel, RgbChannel, RgbChannel] | None,
1076+
Field(serialization_alias="text_color_rgb", default=None),
1077+
BeforeValidator(_split_comma_string),
1078+
PlainSerializer(_serialize_as_comma_separated_string),
1079+
] = None
1080+
text_color_cmyk: Annotated[
1081+
tuple[CmykChannel, CmykChannel, CmykChannel, CmykChannel] | None,
1082+
Field(serialization_alias="text_color_cmyk", default=None),
1083+
BeforeValidator(_split_comma_string),
1084+
PlainSerializer(_serialize_as_comma_separated_string),
1085+
] = None
1086+
watermark_file_scale: Annotated[
1087+
float, Field(serialization_alias="watermark_file_scale", ge=0, default=0.5)
1088+
] = 0.5
1089+
opacity: Annotated[
1090+
float, Field(serialization_alias="opacity", ge=0, le=1, default=0.5)
1091+
] = 0.5
1092+
horizontal_alignment: Annotated[
1093+
WatermarkHorizontalAlignment,
1094+
Field(serialization_alias="horizontal_alignment", default="center"),
1095+
] = "center"
1096+
vertical_alignment: Annotated[
1097+
WatermarkVerticalAlignment,
1098+
Field(serialization_alias="vertical_alignment", default="center"),
1099+
] = "center"
1100+
x: Annotated[int, Field(serialization_alias="x", default=0)] = 0
1101+
y: Annotated[int, Field(serialization_alias="y", default=0)] = 0
1102+
rotation: Annotated[int, Field(serialization_alias="rotation", default=0)] = 0
1103+
pages: Annotated[
1104+
list[AscendingPageRange] | None,
1105+
Field(serialization_alias="pages", min_length=1, default=None),
1106+
BeforeValidator(_ensure_list),
1107+
BeforeValidator(_split_comma_list),
1108+
BeforeValidator(_int_to_string),
1109+
PlainSerializer(_serialize_page_ranges),
1110+
] = None
1111+
behind_page: Annotated[
1112+
bool, Field(serialization_alias="behind_page", default=False)
1113+
] = False
1114+
1115+
@model_validator(mode="after")
1116+
def _validate_watermark_payload(self) -> PdfWatermarkPayload:
1117+
has_text = self.watermark_text is not None
1118+
has_file = self.watermark_file is not None
1119+
if has_text == has_file:
1120+
msg = "Provide exactly one of watermark_text or watermark_file."
1121+
raise ValueError(msg)
1122+
if self.text_color_rgb is not None and self.text_color_cmyk is not None:
1123+
msg = "Specify only one of text_color_rgb or text_color_cmyk."
1124+
raise ValueError(msg)
1125+
return self
1126+
1127+
10241128
class PdfXfaToAcroformsPayload(BaseModel):
10251129
"""Adapt caller options into a pdfRest-ready XFA-to-AcroForms request payload."""
10261130

src/pdfrest/types/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
JpegColorModel,
1313
OcrLanguage,
1414
PdfAType,
15+
PdfCMYKColor,
1516
PdfInfoQuery,
1617
PdfMergeInput,
1718
PdfMergeSource,
@@ -27,6 +28,8 @@
2728
SummaryOutputType,
2829
TiffColorModel,
2930
TranslateOutputFormat,
31+
WatermarkHorizontalAlignment,
32+
WatermarkVerticalAlignment,
3033
)
3134

3235
__all__ = [
@@ -41,6 +44,7 @@
4144
"JpegColorModel",
4245
"OcrLanguage",
4346
"PdfAType",
47+
"PdfCMYKColor",
4448
"PdfInfoQuery",
4549
"PdfMergeInput",
4650
"PdfMergeSource",
@@ -56,4 +60,6 @@
5660
"SummaryOutputType",
5761
"TiffColorModel",
5862
"TranslateOutputFormat",
63+
"WatermarkHorizontalAlignment",
64+
"WatermarkVerticalAlignment",
5965
]

src/pdfrest/types/public.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"JpegColorModel",
2525
"OcrLanguage",
2626
"PdfAType",
27+
"PdfCMYKColor",
2728
"PdfInfoQuery",
2829
"PdfMergeInput",
2930
"PdfMergeSource",
@@ -39,6 +40,8 @@
3940
"SummaryOutputType",
4041
"TiffColorModel",
4142
"TranslateOutputFormat",
43+
"WatermarkHorizontalAlignment",
44+
"WatermarkVerticalAlignment",
4245
)
4346

4447
PdfInfoQuery = Literal[
@@ -102,6 +105,7 @@ class PdfRedactionInstruction(TypedDict):
102105
value: PdfRedactionPreset | str
103106

104107

108+
PdfCMYKColor = tuple[int, int, int, int]
105109
PdfRGBColor = tuple[int, int, int]
106110

107111
PdfPageSelection = str | int | Sequence[str | int]
@@ -160,3 +164,6 @@ class PdfMergeSource(TypedDict, total=False):
160164
ALL_OCR_LANGUAGES: tuple[OcrLanguage, ...] = cast(
161165
tuple[OcrLanguage, ...], get_args(OcrLanguage)
162166
)
167+
168+
WatermarkHorizontalAlignment = Literal["left", "center", "right"]
169+
WatermarkVerticalAlignment = Literal["top", "center", "bottom"]

0 commit comments

Comments
 (0)