Skip to main content
Version: API v2 Preview

Working with Text

To edit text, describe what PDFDancer should target and choose an operation: replace, insert, delete, or style. The operation returns a TextEditResponse describing what PDFDancer found and changed.

Download the quickstart PDF as input.pdf. It contains Hello on page 1. The complete example below writes output.pdf with Hello replaced by Final.

The basic workflow

from pathlib import Path
from pdfdancer import PDFDancer, TextReplaceRequest

with PDFDancer.open(Path("input.pdf")) as pdf:
response = pdf.text().replace(
TextReplaceRequest.literal("Hello", "Final")
.whole_words(True)
.build()
)

if response.matched == 0:
raise RuntimeError("Hello was not found")
if response.errors:
raise RuntimeError(f"Replacement failed: {response.errors}")

pdf.save("output.pdf")

Document scope and page scope

pdf.text() searches the document. pdf.page(2).text() limits the operation to page 2. Page numbers are one-based.

You can also limit a document-wide edit to a set of page numbers. Prefer pdf.page(n).text() when one page is the natural boundary; configure page numbers on the edit definition when the same change should run on several known pages.

page_two = pdf.page(2).text().replace(
TextReplaceRequest.literal("Draft", "Final").build()
)
selected_pages = pdf.text().replace(
TextReplaceRequest.literal("Draft", "Final").pages(2, 4).build()
)

Choose a selector

Literal selectors are safest for known text. Regular expressions are useful for variable content such as invoice numbers.

literal = TextReplaceRequest.literal("Acme Ltd", "Northwind GmbH").build()
invoice = (
TextReplaceRequest.regex(r"Invoice\s+#\d+", "Invoice")
.case_sensitive(False)
.max_matches(1)
.build()
)

wholeWords applies to literal selectors. maxMatches limits the number of eligible matches, which is useful protection for broad selectors.

Replace, insert, delete, and style

Each operation has a dedicated builder. These examples show one minimal edit of each kind.

from pdfdancer import (
PdfColorRequest,
TextDeleteRequest,
TextInsertRequest,
TextReplaceRequest,
TextStyleRequest,
)

replaced = pdf.text().replace(
TextReplaceRequest.literal("Draft", "Final").build()
)
inserted = pdf.text().insert(
TextInsertRequest.after("Invoice total", " including tax").build()
)
deleted = pdf.page(2).text().delete(
TextDeleteRequest.literal("Internal only").build()
)
styled = pdf.text().style(
TextStyleRequest.literal("Overdue")
.fill_color(PdfColorRequest.rgb(0.85, 0.1, 0.1))
.build()
)

Replacement and anchor insertion inherit appearance from existing text. Coordinate insertion requires an explicit font and size. Styling changes appearance without changing characters. Deletion removes selected text; it is not a security redaction operation.

Read the response

FieldMeaning
matchedNumber of selector matches or insertion targets
changedNumber of changes actually applied
pagesChangedOne-based page numbers changed
changePer-change details, including requested and applied layout
warningsNonfatal conditions, including permitted layout fallback
errorsRequested changes that were not applied

matched > 0 is not sufficient for success. If changed < matched, inspect warnings and errors before saving.

if response.matched == 0:
raise RuntimeError("Required text was not found")
if response.errors or response.changed != response.matched:
raise RuntimeError(f"Not every match changed: {response.errors}")
for warning in response.warnings or ():
print(warning.code, warning.page, warning.message)

When visible text does not match

PDF text is stored as positioned glyphs and runs. A phrase that looks continuous in a viewer may be split internally. If a literal selector finds nothing:

  1. Confirm the page scope and capitalization.
  2. Try a smaller, unique literal.
  3. Inspect the source PDF with a PDF viewer or extraction tool to determine how its visible text is encoded.
  4. Use a narrowly bounded regular expression.

Continue with Replace, Insert, and Delete, Styling Text, or Text Layout.