Skip to content

Commit 3a02f90

Browse files
Add Add Text methods
Assisted-by: Codex
1 parent 75f561c commit 3a02f90

4 files changed

Lines changed: 237 additions & 0 deletions

File tree

src/pdfrest/client.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
OcrPdfPayload,
8484
PdfAddAttachmentPayload,
8585
PdfAddImagePayload,
86+
PdfAddTextPayload,
8687
PdfCompressPayload,
8788
PdfFlattenAnnotationsPayload,
8889
PdfFlattenFormsPayload,
@@ -117,6 +118,7 @@
117118
GraphicSmoothing,
118119
JpegColorModel,
119120
OcrLanguage,
121+
PdfAddTextObject,
120122
PdfAType,
121123
PdfInfoQuery,
122124
PdfMergeInput,
@@ -2501,6 +2503,36 @@ def apply_redactions(
25012503
timeout=timeout,
25022504
)
25032505

2506+
def add_text_to_pdf(
2507+
self,
2508+
file: PdfRestFile | Sequence[PdfRestFile],
2509+
*,
2510+
text_objects: PdfAddTextObject | Sequence[PdfAddTextObject],
2511+
output: str | None = None,
2512+
extra_query: Query | None = None,
2513+
extra_headers: AnyMapping | None = None,
2514+
extra_body: Body | None = None,
2515+
timeout: TimeoutTypes | None = None,
2516+
) -> PdfRestFileBasedResponse:
2517+
"""Insert one or more text blocks into a PDF."""
2518+
2519+
payload: dict[str, Any] = {
2520+
"files": file,
2521+
"text_objects": text_objects,
2522+
}
2523+
if output is not None:
2524+
payload["output"] = output
2525+
2526+
return self._post_file_operation(
2527+
endpoint="/pdf-with-added-text",
2528+
payload=payload,
2529+
payload_model=PdfAddTextPayload,
2530+
extra_query=extra_query,
2531+
extra_headers=extra_headers,
2532+
extra_body=extra_body,
2533+
timeout=timeout,
2534+
)
2535+
25042536
def add_image_to_pdf(
25052537
self,
25062538
file: PdfRestFile | Sequence[PdfRestFile],
@@ -3564,6 +3596,36 @@ async def apply_redactions(
35643596
timeout=timeout,
35653597
)
35663598

3599+
async def add_text_to_pdf(
3600+
self,
3601+
file: PdfRestFile | Sequence[PdfRestFile],
3602+
*,
3603+
text_objects: PdfAddTextObject | Sequence[PdfAddTextObject],
3604+
output: str | None = None,
3605+
extra_query: Query | None = None,
3606+
extra_headers: AnyMapping | None = None,
3607+
extra_body: Body | None = None,
3608+
timeout: TimeoutTypes | None = None,
3609+
) -> PdfRestFileBasedResponse:
3610+
"""Asynchronously insert text blocks into a PDF."""
3611+
3612+
payload: dict[str, Any] = {
3613+
"files": file,
3614+
"text_objects": text_objects,
3615+
}
3616+
if output is not None:
3617+
payload["output"] = output
3618+
3619+
return await self._post_file_operation(
3620+
endpoint="/pdf-with-added-text",
3621+
payload=payload,
3622+
payload_model=PdfAddTextPayload,
3623+
extra_query=extra_query,
3624+
extra_headers=extra_headers,
3625+
extra_body=extra_body,
3626+
timeout=timeout,
3627+
)
3628+
35673629
async def add_image_to_pdf(
35683630
self,
35693631
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ def _serialize_redactions(value: list[_PdfRedactionVariant]) -> str:
148148
return json.dumps(payload, separators=(",", ":"))
149149

150150

151+
def _serialize_text_objects(value: list[BaseModel]) -> str:
152+
payload = [entry.model_dump(mode="json", exclude_none=True) for entry in value]
153+
return json.dumps(payload, separators=(",", ":"))
154+
155+
151156
def _allowed_mime_types(
152157
allowed_mime_types: str, *more_allowed_mime_types: str, error_msg: str | None
153158
) -> Callable[[Any], Any]:
@@ -563,6 +568,64 @@ class ExtractImagesPayload(BaseModel):
563568

564569

565570
RgbChannel = Annotated[int, Field(ge=0, le=255)]
571+
CmykChannel = Annotated[int, Field(ge=0, le=100)]
572+
573+
574+
def _validate_rgb_values(value: list[Any] | tuple[Any, ...] | None) -> list[int] | None:
575+
if value is None:
576+
return None
577+
if len(value) != 3:
578+
msg = "text_color_rgb must have exactly 3 values."
579+
raise ValueError(msg)
580+
channels: list[int] = []
581+
for channel in value:
582+
try:
583+
numeric = int(channel)
584+
except (TypeError, ValueError) as exc:
585+
msg = "text_color_rgb values must be integers."
586+
raise ValueError(msg) from exc
587+
if not 0 <= numeric <= 255:
588+
msg = "text_color_rgb values must be between 0 and 255."
589+
raise ValueError(msg)
590+
channels.append(numeric)
591+
return channels
592+
593+
594+
def _validate_cmyk_values(
595+
value: list[Any] | tuple[Any, ...] | None,
596+
) -> list[int] | None:
597+
if value is None:
598+
return None
599+
if len(value) != 4:
600+
msg = "text_color_cmyk must have exactly 4 values."
601+
raise ValueError(msg)
602+
channels: list[int] = []
603+
for channel in value:
604+
try:
605+
numeric = int(channel)
606+
except (TypeError, ValueError) as exc:
607+
msg = "text_color_cmyk values must be integers."
608+
raise ValueError(msg) from exc
609+
if not 0 <= numeric <= 100:
610+
msg = "text_color_cmyk values must be between 0 and 100."
611+
raise ValueError(msg)
612+
channels.append(numeric)
613+
return channels
614+
615+
616+
def _validate_add_text_page(value: str | int) -> str | int:
617+
if isinstance(value, str):
618+
if value.lower() == "all":
619+
return "all"
620+
if value.isdigit():
621+
numeric_page = int(value)
622+
if numeric_page >= 1:
623+
return numeric_page
624+
else:
625+
if value >= 1:
626+
return value
627+
msg = 'page must be a positive integer or "all".'
628+
raise ValueError(msg)
566629

567630

568631
class PdfLiteralRedactionModel(BaseModel):
@@ -1021,6 +1084,94 @@ def _validate_profile_dependency(self) -> PdfCompressPayload:
10211084
return self
10221085

10231086

1087+
class PdfAddTextObjectModel(BaseModel):
1088+
"""Adapt caller text options for insertion into a PDF."""
1089+
1090+
font: Annotated[str, Field(min_length=1, serialization_alias="font")]
1091+
max_width: Annotated[
1092+
float,
1093+
Field(serialization_alias="max_width", gt=0),
1094+
]
1095+
opacity: Annotated[
1096+
float,
1097+
Field(serialization_alias="opacity", ge=0.0, le=1.0),
1098+
]
1099+
page: Annotated[
1100+
str | int,
1101+
Field(serialization_alias="page"),
1102+
AfterValidator(_validate_add_text_page),
1103+
]
1104+
rotation: Annotated[float, Field(serialization_alias="rotation")]
1105+
text: Annotated[str, Field(min_length=1, serialization_alias="text")]
1106+
text_color_rgb: Annotated[
1107+
list[int] | tuple[int, ...] | None,
1108+
Field(serialization_alias="text_color_rgb", default=None),
1109+
BeforeValidator(_split_comma_string),
1110+
AfterValidator(_validate_rgb_values),
1111+
PlainSerializer(_serialize_as_comma_separated_string),
1112+
] = None
1113+
text_color_cmyk: Annotated[
1114+
list[int] | tuple[int, ...] | None,
1115+
Field(serialization_alias="text_color_cmyk", default=None),
1116+
BeforeValidator(_split_comma_string),
1117+
AfterValidator(_validate_cmyk_values),
1118+
PlainSerializer(_serialize_as_comma_separated_string),
1119+
] = None
1120+
text_size: Annotated[
1121+
float,
1122+
Field(serialization_alias="text_size", ge=5, le=100),
1123+
]
1124+
x: Annotated[float, Field(serialization_alias="x")]
1125+
y: Annotated[float, Field(serialization_alias="y")]
1126+
is_rtl: Annotated[
1127+
bool | None,
1128+
Field(serialization_alias="is_rtl", default=None),
1129+
] = None
1130+
1131+
@model_validator(mode="after")
1132+
def _ensure_single_color_option(self) -> PdfAddTextObjectModel:
1133+
if self.text_color_rgb is None and self.text_color_cmyk is None:
1134+
msg = "Either text_color_rgb or text_color_cmyk must be provided."
1135+
raise ValueError(msg)
1136+
if self.text_color_rgb is not None and self.text_color_cmyk is not None:
1137+
msg = "Provide only one of text_color_rgb or text_color_cmyk."
1138+
raise ValueError(msg)
1139+
return self
1140+
1141+
1142+
class PdfAddTextPayload(BaseModel):
1143+
"""Adapt caller options into a pdfRest-ready add-text request payload."""
1144+
1145+
files: Annotated[
1146+
list[PdfRestFile],
1147+
Field(
1148+
min_length=1,
1149+
max_length=1,
1150+
validation_alias=AliasChoices("file", "files"),
1151+
serialization_alias="id",
1152+
),
1153+
BeforeValidator(_ensure_list),
1154+
AfterValidator(
1155+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
1156+
),
1157+
PlainSerializer(_serialize_as_first_file_id),
1158+
]
1159+
text_objects: Annotated[
1160+
list[PdfAddTextObjectModel],
1161+
Field(
1162+
serialization_alias="text_objects",
1163+
min_length=1,
1164+
),
1165+
BeforeValidator(_ensure_list),
1166+
PlainSerializer(_serialize_text_objects),
1167+
]
1168+
output: Annotated[
1169+
str | None,
1170+
Field(serialization_alias="output", min_length=1, default=None),
1171+
AfterValidator(_validate_output_prefix),
1172+
] = None
1173+
1174+
10241175
class PdfAddImagePayload(BaseModel):
10251176
"""Adapt caller options into a pdfRest-ready add-image request payload."""
10261177

src/pdfrest/types/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
GraphicSmoothing,
1212
JpegColorModel,
1313
OcrLanguage,
14+
PdfAddTextObject,
1415
PdfAType,
16+
PdfCmykColor,
1517
PdfInfoQuery,
1618
PdfMergeInput,
1719
PdfMergeSource,
@@ -41,6 +43,8 @@
4143
"JpegColorModel",
4244
"OcrLanguage",
4345
"PdfAType",
46+
"PdfAddTextObject",
47+
"PdfCmykColor",
4448
"PdfInfoQuery",
4549
"PdfMergeInput",
4650
"PdfMergeSource",

src/pdfrest/types/public.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"JpegColorModel",
2525
"OcrLanguage",
2626
"PdfAType",
27+
"PdfAddTextObject",
28+
"PdfCmykColor",
2729
"PdfInfoQuery",
2830
"PdfMergeInput",
2931
"PdfMergeSource",
@@ -104,6 +106,24 @@ class PdfRedactionInstruction(TypedDict):
104106

105107
PdfRGBColor = tuple[int, int, int]
106108

109+
PdfCmykColor = tuple[int, int, int, int]
110+
111+
112+
class PdfAddTextObject(TypedDict, total=False):
113+
font: Required[str]
114+
max_width: Required[float | int | str]
115+
opacity: Required[float | int | str]
116+
page: Required[int | str]
117+
rotation: Required[float | int | str]
118+
text: Required[str]
119+
text_color_rgb: PdfRGBColor
120+
text_color_cmyk: PdfCmykColor
121+
text_size: Required[float | int | str]
122+
x: Required[float | int | str]
123+
y: Required[float | int | str]
124+
is_rtl: bool
125+
126+
107127
PdfPageSelection = str | int | Sequence[str | int]
108128

109129

0 commit comments

Comments
 (0)