Work in progress — this reference is being written in the open. Unfinished pages are excluded from search engines.
Paged · IDML Reference
Typography

Justification and line-breaking

The Justification values, how the composer breaks a paragraph into lines, and where its line-breaking and hyphenation diverge from InDesign.

Pro· explanation

A paragraph's Justification sets how its lines align in the column; the composer then breaks the whole paragraph with a Knuth–Plass total-fit pass, while penalty-weight and hyphenation calibration against InDesign is still ongoing.

In short: Justification chooses a paragraph's alignment — left, centre, right, or one of the justified modes that stretch inter-word space to fill the line. The composer feeds that choice into the harder question of where to break each line, building a Knuth–Plass item stream and running a total-fit pass that minimises badness across the entire paragraph at once, the way InDesign's Paragraph Composer does. The shape of the algorithm matches InDesign, but the exact penalty weights — and the TeX-pattern hyphenation that stands in for InDesign's licensed dictionaries — are still being calibrated, so line breaks are close but not yet guaranteed identical on every paragraph.

A paragraph's Justification says how its lines align in the column, and that choice feeds into a harder question the composer answers next: where to break each line so the chosen alignment looks good. This page covers both — the alignment values first, then how the line-breaker and hyphenator actually behave, including where they knowingly differ from InDesign.

The Justification values

Justification rides on the ParagraphStyleRange (and on ParagraphStyle). It distinguishes alignment-only modes from justified modes — the latter stretch the space between words to fill the line:

Attribute · ParagraphStyleRangeType / valuesSupportNotes
Justification = LeftAlignenumSupportedFlush left, ragged right. The IDML default.
Justification = CenterAlignenumSupportedCentred, ragged both sides.
Justification = RightAlignenumSupportedFlush right, ragged left.
Justification = LeftJustifiedenumSupportedJustified, last line flush left.
Justification = CenterJustifiedenumSupportedComposed as Center (the centred-last-line variant maps to Center alignment).
Justification = RightJustifiedenumSupportedComposed as Right (maps to Right alignment).
Justification = FullyJustifiedenumSupportedEvery line, including the last, stretched. Currently composed the same as LeftJustified.
Justification = ToBindingSideenumParsed, not yet renderedBinding-aware; falls back to LeftAlign until binding side is plumbed through.
Justification = AwayFromBindingSideenumParsed, not yet renderedBinding-aware; falls back to RightAlign.

The value is parsed into a typed enum at read time, so an unrecognised string is not silently misaligned — it simply resolves to the left-aligned default. Two points of honesty in the table above: FullyJustified is composed the same as LeftJustified today (the distinction round-trips losslessly but does not yet change the last line), and the two binding-aware values fall back to plain left/right because binding side is a document-level setting the layout does not yet read. Parsed, not yet renderedToBindingSide / AwayFromBindingSide fall back to Left / Right; binding side not yet plumbed through. core/crates/paged-parse/src/story.rs

How the composer breaks lines

The composer does not break greedily line by line. It builds a Knuth–Plass item stream for the whole paragraph — boxes (glyph runs), glue (inter-word space with a desired width and a stretch/shrink band), and penalties (break opportunities) — and runs a total-fit pass that minimises the badness across the entire paragraph at once, the way InDesign's Paragraph Composer does. That is why moving one word can re-flow lines above it: the algorithm is weighing all the breaks together, not committing to each line in isolation.

The space band is shaped by the paragraph's spacing preferences: the desired word spacing scales the natural space width, and stretch/shrink ratios set how far that space may grow or compress before a break is preferred elsewhere. When the breaker can't find a feasible set of breaks at the configured tolerance — for example a single token wider than the column — it relaxes the tolerance and tries again rather than failing the paragraph.

Calibration is ongoing

The shape of the algorithm matches InDesign; the exact weights — tolerance, looseness, and the glue stretch/shrink ratios — are still being calibrated against InDesign's output on a reference corpus. So line breaks are close to InDesign's but not yet guaranteed identical on every paragraph; expect occasional divergence in where a specific line ends until the penalty weights are locked in. SupportedKnuth–Plass total-fit is implemented and used; penalty-weight calibration against InDesign's Paragraph Composer is in progress. core/crates/paged-text/src/compose.rs

Hyphenation is a heuristic

When hyphenation is enabled, the composer can break inside a word at a hyphenation point, paying a penalty so it only does so when it improves the paragraph fit. The hyphenation points themselves come from TeX (Liang) patterns, by language — a compact, embedded pattern set that needs no external dictionary and works the same on native and WASM builds.

InDesign uses Proximity's licensed hyphenation dictionaries, which we do not ship. The TeX patterns are the same family of algorithm and agree the vast majority of the time, but on some words they place a break one syllable differently. The result is a small, known divergence in where a word hyphenates — not in whether the paragraph is correct. SupportedHyphenation via TeX patterns (hypher); Proximity dictionary not licensed — minor break-position divergence vs InDesign. core/crates/paged-text/src/hyphenate.rs

A related gap sits on the CJK side. The composer enforces a small built-in set of hard kinsoku (line-break-forbidden) rules — roughly the characters that may not start or end a line — but it does not consult IDML's named KinsokuSet tables or the full JIS X 4051 line-composition rules. The paragraph's KinsokuType toggles the built-in enforcement on; the finer per-set flavour is not yet wired. Parsed, not yet renderedKinsokuSet / MojikumiTable references parsed onto the paragraph; only a built-in hard-kinsoku heuristic is enforced, not the named tables. core/crates/paged-parse/src/story.rs

Frequently asked questions

Does the composer break lines greedily, one line at a time? No. It builds a Knuth–Plass item stream for the whole paragraph — boxes, glue, and penalties — and runs a total-fit pass that minimises badness across the entire paragraph at once, the way InDesign's Paragraph Composer does. That is why moving one word can re-flow lines above it.

Will Paged's line breaks match InDesign's exactly? Not yet on every paragraph. The shape of the algorithm matches InDesign, but the exact weights — tolerance, looseness, and the glue stretch/shrink ratios — are still being calibrated against InDesign's output on a reference corpus. Expect breaks that are close but occasionally diverge until the penalty weights are locked in.

Why might a word hyphenate at a slightly different point than in InDesign? Paged hyphenates using embedded TeX (Liang) patterns by language, while InDesign uses Proximity's licensed dictionaries, which we do not ship. The two are the same family of algorithm and agree most of the time, but on some words they place a break one syllable differently — a small, known divergence in where a word breaks, not in whether the paragraph is correct.

Are all the Justification values rendered as InDesign composes them? Most are. Left, centre, and right alignment and LeftJustified are honoured; CenterJustified and RightJustified map to centre and right alignment. FullyJustified round-trips losslessly but is currently composed the same as LeftJustified, and the binding-aware ToBindingSide / AwayFromBindingSide values fall back to plain left/right until binding side is plumbed through.

On this page