Skip to main content
Version: API v2 Preview

Text Layout and Reflow

Changing the length of text creates a layout decision. If Q1 becomes First quarter, PDFDancer must either leave all unaffected text at its existing coordinates or recompose the text around the change.

Layout has two independent settings:

  • The layout mode decides whether PDFDancer attempts reflow and whether it may fall back when reflow is unavailable.
  • The reflow profile decides how PDFDancer composes a detected text unit when the selected mode attempts reflow.

A text unit is the detected layout structure that owns the selected characters, such as a paragraph, list item, heading, table cell, or fixed-layout line.

Compare the results

The diagram shows the same replacement under the three composition behaviors.

Source-anchored, no-reflow, and body-text results after replacing Q1 with First quarter

BehaviorWhat movesCan line breaks change?Main risk
Source anchoredOnly the inserted or replacement textNoLonger text can overlap unchanged glyphs
Reflow with NO_REFLOWFollowing text on each existing lineNoThe line can extend beyond its original width
Reflow with BODY_TEXTText in the detected unitYesAn unsuitable unit can be composed like prose

Choose a layout mode

sourceAnchored()

The new text begins at the selected source position. Every unaffected glyph remains at its original coordinates. This is also the default when no layout mode is specified.

Use it for same-length or shorter substitutions, isolated labels with sufficient empty space, and coordinate insertion. Do not use it for a longer inline replacement when following text must move.

reflowWhenSupported(profile)

PDFDancer attempts to recompose the detected text unit with the selected profile. If that unit cannot be reflowed, PDFDancer applies the edit source-anchored and adds a warning to TextEditResponse.

Choose this only when the source-anchored fallback is acceptable. A successful change count does not prove that reflow occurred; inspect the applied layout reported for each change.

requireReflow(profile)

PDFDancer attempts the same reflow operation but does not permit a source-anchored fallback. If a selected unit cannot be reflowed, that target is left unchanged and the response contains a text-reflow error.

Choose this when overlap or fixed-position fallback would make the output unacceptable.

Fallback behavior for source anchored, reflow when supported, and require reflow

Configure each mode

These examples show the three choices for the same replacement. Execute only the choice required by your workflow.

from pdfdancer import TextLayoutProfile, TextReplaceRequest

source_anchored = TextReplaceRequest.literal("Q1", "First quarter") \
.source_anchored() \
.build()

fallback_allowed = TextReplaceRequest.literal("Q1", "First quarter") \
.reflow_when_supported(TextLayoutProfile.BODY_TEXT) \
.build()

reflow_required = TextReplaceRequest.literal("Q1", "First quarter") \
.require_reflow(TextLayoutProfile.BODY_TEXT) \
.build()

response = pdf.text().replace(reflow_required)

Choose a reflow profile

A profile is required by reflowWhenSupported and requireReflow. It is not valid with sourceAnchored.

DEFAULT

DEFAULT chooses a composition strategy from the detected layout type:

  • paragraphs use body-text composition;
  • lists use composition appropriate to their detected alignment;
  • protected or fixed-layout units use the no-reflow profile.

Protected units include headings, labels, tables, tables of contents, callouts, structured or preformatted text, headers, footers, footnotes, page numbers, watermarks, and unknown layouts.

Use DEFAULT when one operation can match different kinds of content and PDFDancer should select the profile for each detected unit.

BODY_TEXT

BODY_TEXT uses paragraph composition for every selected unit. It recalculates line breaks and can adjust tracking and word spacing within the profile's constraints while preserving a readable paragraph measure.

Use it for known prose. Do not force it onto headings, table cells, code, or preformatted text unless paragraph composition is explicitly the intended result.

NO_REFLOW

NO_REFLOW recomposes each existing line horizontally while preserving every original line boundary. Following text moves to make room for a longer replacement, but it never wraps to another line. The resulting line can extend beyond its original width.

This differs from sourceAnchored: NO_REFLOW moves following text; sourceAnchored leaves it fixed.

Profile examples

mixed_content = TextReplaceRequest.literal("Draft", "Final") \
.reflow_when_supported(TextLayoutProfile.DEFAULT) \
.build()

known_prose = TextReplaceRequest.literal("Q1", "First quarter") \
.require_reflow(TextLayoutProfile.BODY_TEXT) \
.build()

fixed_lines = TextReplaceRequest.literal("Q1", "First quarter") \
.reflow_when_supported(TextLayoutProfile.NO_REFLOW) \
.build()

Control generated hyphenation

hyphenationEnabled(true|false) overrides dictionary-generated discretionary hyphenation for one edit. If omitted, the selected profile determines the effective setting.

The override does not remove or add lexical hyphens present in the supplied text. It is valid only with reflowWhenSupported or requireReflow; it cannot be combined with sourceAnchored.

edit = TextReplaceRequest.literal("Q1", "First quarter") \
.require_reflow(TextLayoutProfile.BODY_TEXT) \
.hyphenation_enabled(False) \
.build()

Verify the applied layout

TextEditResponse distinguishes the selected target, the applied change, and the layout actually used:

FieldMeaning
matchedNumber of eligible targets found
changedNumber of changes applied
changePer-change details, including requested mode, requested profile, applied mode, and effective hyphenation
warningsNonfatal diagnostics, including permitted source-anchored fallback
errorsWork that was not applied, including failed required reflow

For reflowWhenSupported, reject an applied mode of sourceAnchored when your application cannot accept fallback. For requireReflow, reject errors and any difference between matched and changed.

if response.errors or response.matched != response.changed:
raise RuntimeError(f"Text edit was incomplete: {response.errors}")

for change in response.change or ():
print(
change.page,
change.requested_layout_mode,
change.requested_layout_profile,
change.applied_layout_mode,
change.effective_hyphenation_enabled,
)

Decision table

Required outputConfiguration
Unaffected glyphs must remain at their exact coordinatessourceAnchored()
Mixed content may fall back to fixed coordinatesreflowWhenSupported(DEFAULT)
Known prose may fall back to fixed coordinatesreflowWhenSupported(BODY_TEXT)
Known prose must never use fixed-coordinate fallbackrequireReflow(BODY_TEXT)
Following text should move, but existing line breaks must remainA reflow mode with NO_REFLOW

Always save geometry-sensitive edits to a separate output file and inspect the rendered PDF before applying the same operation in bulk.