Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Use this group to add visible content or remove sensitive content.
[add_text_to_pdf][pdfrest.PdfRestClient.add_text_to_pdf],
[add_image_to_pdf][pdfrest.PdfRestClient.add_image_to_pdf],
[add_shapes_to_pdf][pdfrest.PdfRestClient.add_shapes_to_pdf]
(`add_text_to_pdf` can tag newly added text and identify its natural language
for assistive technology)
- Watermarking:
[watermark_pdf_with_text][pdfrest.PdfRestClient.watermark_pdf_with_text],
[watermark_pdf_with_image][pdfrest.PdfRestClient.watermark_pdf_with_image]
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ supported interpreter matrix.
hosted TDM policy URL in an uploaded PDF.
- `examples/set_pdf_page_boxes/set_pdf_page_boxes_example.py` – enlarge a PDF's
MediaBox and apply a CropBox inset with typed page selections and margins.
- `examples/add_text/add_tagged_text_to_pdf_example.py` – add an accessible text
overlay with tagging enabled and a BCP 47 language tag.
- `examples/add_shapes/add_shapes_to_pdf_example.py` – add a styled rectangle
and divider line to a PDF with accessibility tagging enabled.
- `examples/convert_structured_documents/convert_structured_documents_to_pdf_example.py`
Expand Down
66 changes: 66 additions & 0 deletions examples/add_text/add_tagged_text_to_pdf_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# /// script
# requires-python = ">=3.10"
# dependencies = ["pdfrest", "python-dotenv"]
# ///
"""Add tagged English text to a PDF for accessible reading and extraction.

This sample demonstrates how to:

1. Upload the bundled ``examples/resources/report.pdf`` resource.
2. Describe a text overlay with the typed ``PdfAddTextObject`` contract.
3. Enable tagging and identify the added text with a BCP 47 language tag.
4. Print metadata for the PDF returned by pdfRest.

Set ``PDFREST_API_KEY``, then run from the repository root with
``uv run examples/add_text/add_tagged_text_to_pdf_example.py``. The input PDF
is included in the repository, so no additional input files are required.
"""

from __future__ import annotations

from pathlib import Path

from dotenv import load_dotenv

from pdfrest import PdfRestClient
from pdfrest.types import PdfAddTextObject

RESOURCE = Path(__file__).resolve().parents[1] / "resources" / "report.pdf"


def add_tagged_text_to_report() -> None:
"""Upload the sample report and add an accessible English heading."""
load_dotenv()
heading = PdfAddTextObject(
font="Arial Bold",
max_width=468,
opacity=1,
page=1,
rotation=0,
text="Quarterly report",
text_color_rgb=(26, 72, 112),
text_size=24,
x=72,
y=684,
)

with PdfRestClient() as client:
uploaded = client.files.create_from_paths([RESOURCE])[0]
response = client.add_text_to_pdf(
uploaded,
text_objects=heading,
tag_enabled=True,
tag_language="en-US",
output="report-with-tagged-text",
)

output = response.output_file
print(f"Created {output.name}")
print(f"Output ID: {output.id}")
print(f"MIME type: {output.type}")
print(f"Size: {output.size} bytes")
print(f"Download URL: {output.url}")


if __name__ == "__main__": # pragma: no cover - manual example
add_tagged_text_to_report()
26 changes: 24 additions & 2 deletions src/pdfrest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3434,6 +3434,8 @@ def add_text_to_pdf(
file: PdfRestFile | Sequence[PdfRestFile],
*,
text_objects: PdfAddTextObject | Sequence[PdfAddTextObject],
tag_enabled: bool | None = None,
tag_language: str | None = None,
output: str | None = None,
extra_query: Query | None = None,
extra_headers: AnyMapping | None = None,
Expand All @@ -3442,11 +3444,16 @@ def add_text_to_pdf(
) -> PdfRestFileBasedResponse:
"""Insert one or more text blocks into a PDF.

Places one or more text overlays onto a PDF with font, size, color, opacity, position, and rotation controls. This supports annotations, stamps, and templated labeling workflows.
Places one or more text overlays onto a PDF with font, size, color,
opacity, position, rotation, and accessibility tagging controls. Set
``tag_enabled=True`` when supplying ``tag_language``.

Args:
file: Uploaded input file or files as `PdfRestFile` objects.
text_objects: Text overlay objects to draw onto the document.
tag_enabled: Enable tagging for newly added text.
tag_language: BCP 47 language tag for newly added tagged text.
Requires ``tag_enabled=True``.
output: Output filename prefix used by pdfRest when creating files.
extra_query: Additional query parameters merged into the request.
extra_headers: Additional HTTP headers merged into the request.
Expand All @@ -3464,6 +3471,10 @@ def add_text_to_pdf(
"files": file,
"text_objects": text_objects,
}
if tag_enabled is not None:
payload["tag_enabled"] = tag_enabled
if tag_language is not None:
payload["tag_language"] = tag_language
if output is not None:
payload["output"] = output

Expand Down Expand Up @@ -7101,6 +7112,8 @@ async def add_text_to_pdf(
file: PdfRestFile | Sequence[PdfRestFile],
*,
text_objects: PdfAddTextObject | Sequence[PdfAddTextObject],
tag_enabled: bool | None = None,
tag_language: str | None = None,
output: str | None = None,
extra_query: Query | None = None,
extra_headers: AnyMapping | None = None,
Expand All @@ -7109,11 +7122,16 @@ async def add_text_to_pdf(
) -> PdfRestFileBasedResponse:
"""Asynchronous variant of [PdfRestClient.add_text_to_pdf][pdfrest.PdfRestClient.add_text_to_pdf].

Places one or more text overlays onto a PDF with font, size, color, opacity, position, and rotation controls. This supports annotations, stamps, and templated labeling workflows.
Places one or more text overlays onto a PDF with font, size, color,
opacity, position, rotation, and accessibility tagging controls. Set
``tag_enabled=True`` when supplying ``tag_language``.

Args:
file: Uploaded input file or files as `PdfRestFile` objects.
text_objects: Text overlay objects to draw onto the document.
tag_enabled: Enable tagging for newly added text.
tag_language: BCP 47 language tag for newly added tagged text.
Requires ``tag_enabled=True``.
output: Output filename prefix used by pdfRest when creating files.
extra_query: Additional query parameters merged into the request.
extra_headers: Additional HTTP headers merged into the request.
Expand All @@ -7131,6 +7149,10 @@ async def add_text_to_pdf(
"files": file,
"text_objects": text_objects,
}
if tag_enabled is not None:
payload["tag_enabled"] = tag_enabled
if tag_language is not None:
payload["tag_language"] = tag_language
if output is not None:
payload["output"] = output

Expand Down
25 changes: 25 additions & 0 deletions src/pdfrest/models/_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2934,6 +2934,16 @@ def _ensure_single_color_option(self) -> PdfAddTextObjectModel:
return self


def _validate_tag_language(value: str) -> str:
if not re.fullmatch(r"[A-Za-z]{2,3}(?:-(?:[A-Za-z]{4}|[A-Za-z]{2}|\d{3}))?", value):
msg = "tag_language must be a valid BCP 47 language tag, such as en-US."
raise ValueError(msg)
return value


_TagLanguage = Annotated[str, AfterValidator(_validate_tag_language)]


class PdfAddTextPayload(BaseModel):
"""Adapt caller options into a pdfRest-ready add-text request payload."""

Expand All @@ -2960,12 +2970,27 @@ class PdfAddTextPayload(BaseModel):
BeforeValidator(_ensure_list),
PlainSerializer(_serialize_text_objects),
]
tag_enabled: Annotated[
bool | None,
Field(serialization_alias="tag_enabled", default=None),
] = None
tag_language: Annotated[
_TagLanguage | None,
Field(serialization_alias="tag_language", default=None),
] = None
output: Annotated[
str | None,
Field(serialization_alias="output", min_length=1, default=None),
AfterValidator(_validate_output_prefix),
] = None

@model_validator(mode="after")
def _require_tagging_for_language(self) -> PdfAddTextPayload:
if self.tag_language is not None and self.tag_enabled is not True:
msg = "tag_enabled must be true when tag options are provided."
raise ValueError(msg)
return self


class _PdfAddedShapeBaseModel(BaseModel):
"""Shared validation and serialization for shapes added to PDFs."""
Expand Down
11 changes: 11 additions & 0 deletions tests/live/test_live_add_text_to_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ def test_live_add_text_to_pdf(
response = client.add_text_to_pdf(
uploaded_pdf_for_text,
text_objects=[_default_text_object()],
tag_enabled=True,
tag_language="en-US",
output="live-added-text",
)
tagged_info = client.query_pdf_info(response.output_file, queries="tagged")

assert response.output_files
output_file = response.output_file
Expand All @@ -82,6 +85,7 @@ def test_live_add_text_to_pdf(
assert output_file.size > 0
assert response.warning is None
assert uploaded_pdf_for_text.id in response.input_ids
assert tagged_info.tagged is True


@pytest.mark.asyncio
Expand All @@ -97,6 +101,12 @@ async def test_live_async_add_text_to_pdf(
response = await client.add_text_to_pdf(
uploaded_pdf_for_text,
text_objects=[_default_text_object()],
tag_enabled=True,
tag_language="en-US",
)
tagged_info = await client.query_pdf_info(
response.output_file,
queries="tagged",
)

assert response.output_files
Expand All @@ -105,6 +115,7 @@ async def test_live_async_add_text_to_pdf(
assert output_file.size > 0
assert response.warning is None
assert uploaded_pdf_for_text.id in response.input_ids
assert tagged_info.tagged is True


def test_live_add_text_to_pdf_invalid_page(
Expand Down
Loading
Loading