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.
| Behavior | What moves | Can line breaks change? | Main risk |
|---|---|---|---|
| Source anchored | Only the inserted or replacement text | No | Longer text can overlap unchanged glyphs |
Reflow with NO_REFLOW | Following text on each existing line | No | The line can extend beyond its original width |
Reflow with BODY_TEXT | Text in the detected unit | Yes | An 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.
Configure each mode
These examples show the three choices for the same replacement. Execute only the choice required by your workflow.
- Python
- TypeScript
- Java
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)
import {TextLayoutProfile, TextReplaceRequest} from 'pdfdancer-client-typescript';
const sourceAnchored = TextReplaceRequest.literal('Q1', 'First quarter')
.sourceAnchored()
.build();
const fallbackAllowed = TextReplaceRequest.literal('Q1', 'First quarter')
.reflowWhenSupported(TextLayoutProfile.BODY_TEXT)
.build();
const reflowRequired = TextReplaceRequest.literal('Q1', 'First quarter')
.requireReflow(TextLayoutProfile.BODY_TEXT)
.build();
const response = await pdf.text().replace(reflowRequired);
import com.pdfdancer.common.request.TextLayoutRequest;
import com.pdfdancer.common.request.TextReplaceRequest;
var sourceAnchored = TextReplaceRequest.literal("Q1", "First quarter")
.sourceAnchored()
.build();
var fallbackAllowed = TextReplaceRequest.literal("Q1", "First quarter")
.reflowWhenSupported(TextLayoutRequest.Profile.BODY_TEXT)
.build();
var reflowRequired = TextReplaceRequest.literal("Q1", "First quarter")
.requireReflow(TextLayoutRequest.Profile.BODY_TEXT)
.build();
var response = pdf.text().replace(reflowRequired);
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
- Python
- TypeScript
- Java
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()
const mixedContent = TextReplaceRequest.literal('Draft', 'Final')
.reflowWhenSupported(TextLayoutProfile.DEFAULT).build();
const knownProse = TextReplaceRequest.literal('Q1', 'First quarter')
.requireReflow(TextLayoutProfile.BODY_TEXT).build();
const fixedLines = TextReplaceRequest.literal('Q1', 'First quarter')
.reflowWhenSupported(TextLayoutProfile.NO_REFLOW).build();
var mixedContent = TextReplaceRequest.literal("Draft", "Final")
.reflowWhenSupported(TextLayoutRequest.Profile.DEFAULT).build();
var knownProse = TextReplaceRequest.literal("Q1", "First quarter")
.requireReflow(TextLayoutRequest.Profile.BODY_TEXT).build();
var fixedLines = TextReplaceRequest.literal("Q1", "First quarter")
.reflowWhenSupported(TextLayoutRequest.Profile.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.
- Python
- TypeScript
- Java
edit = TextReplaceRequest.literal("Q1", "First quarter") \
.require_reflow(TextLayoutProfile.BODY_TEXT) \
.hyphenation_enabled(False) \
.build()
const edit = TextReplaceRequest.literal('Q1', 'First quarter')
.requireReflow(TextLayoutProfile.BODY_TEXT)
.hyphenationEnabled(false)
.build();
var edit = TextReplaceRequest.literal("Q1", "First quarter")
.requireReflow(TextLayoutRequest.Profile.BODY_TEXT)
.hyphenationEnabled(false)
.build();
Verify the applied layout
TextEditResponse distinguishes the selected target, the applied change, and the layout actually used:
| Field | Meaning |
|---|---|
matched | Number of eligible targets found |
changed | Number of changes applied |
change | Per-change details, including requested mode, requested profile, applied mode, and effective hyphenation |
warnings | Nonfatal diagnostics, including permitted source-anchored fallback |
errors | Work 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.
- Python
- TypeScript
- Java
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,
)
if (response.errors?.length || response.matched !== response.changed) {
throw new Error(`Text edit was incomplete: ${JSON.stringify(response.errors)}`);
}
for (const change of response.change ?? []) {
console.log({
page: change.page,
requestedMode: change.requestedLayoutMode,
requestedProfile: change.requestedLayoutProfile,
appliedMode: change.appliedLayoutMode,
hyphenation: change.effectiveHyphenationEnabled,
});
}
if (response.errors() != null && !response.errors().isEmpty()
|| !response.matched().equals(response.changed())) {
throw new IllegalStateException("Text edit was incomplete: " + response.errors());
}
if (response.change() != null) {
for (var change : response.change()) {
System.out.printf("page=%s requested=%s profile=%s applied=%s hyphenation=%s%n",
change.page(),
change.requestedLayoutMode(),
change.requestedLayoutProfile(),
change.appliedLayoutMode(),
change.effectiveHyphenationEnabled());
}
}
Decision table
| Required output | Configuration |
|---|---|
| Unaffected glyphs must remain at their exact coordinates | sourceAnchored() |
| Mixed content may fall back to fixed coordinates | reflowWhenSupported(DEFAULT) |
| Known prose may fall back to fixed coordinates | reflowWhenSupported(BODY_TEXT) |
| Known prose must never use fixed-coordinate fallback | requireReflow(BODY_TEXT) |
| Following text should move, but existing line breaks must remain | A 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.