You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
textAlign: justify — ignored with registered TTF fonts; mixed runs overlap; lines ending with a forced break are stretched (0.27.0) #186
Version: @formepdf/core / @formepdf/react 0.27.0 (Node 24.21.0, macOS). Downstream: Reactive Resume 6.0.0 renders documents with Forme 0.27, so justified text is affected there too.
textAlign: "justify" has three problems (minimal reproduction below, Lorem ipsum text):
Registered (embedded) TTF fonts are never justified. The same paragraph is justified with the built-in Helvetica but stays ragged-right with a registered TTF (tested with Arimo 400 from Google Fonts), whatever lineBreaking or hyphens. Possibly related to Type0 / Identity-H fonts, where Tw has no effect (ISO 32000-1, 9.3.3).
Mixed runs collide when justified (built-in Helvetica). With bold / italic child <Text> runs inside a justified paragraph, the run that follows a styled run starts too early and touches or overlaps it. A single-style paragraph is fine.
A line ending with a forced break (\n) is stretched. Such lines should not be justified (CSS applies text-align-last to them), but Forme spreads their words across the full width.
Expected: justified lines with any font, no collision between runs, no stretching of lines that end with a forced break.
Reproduction (Arimo.ttf = static Arimo 400 from fonts.gstatic.com):
import{createElementash}from"react";import{Document,Page,View,Text,Font}from"@formepdf/react";import{renderDocument}from"@formepdf/core";import{readFileSync,writeFileSync}from"node:fs";// Arimo 400 static TTF from Google Fonts (any registered TTF behaves the same)Font.register({family: "Arimo",src: newUint8Array(readFileSync("Arimo.ttf")),fontWeight: 400});constP="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam.";constbox=(label,style,kids)=>h(View,{style: {marginBottom: 14,width: 300}},h(Text,{style: {fontSize: 8,color: "#c00"}},label),h(Text,{style: {fontSize: 10,textAlign: "justify", ...style}}, ...kids));constB=(t)=>h(Text,{style: {fontWeight: 700}},t),I=(t)=>h(Text,{style: {fontStyle: "italic"}},t);constdoc=h(Document,null,h(Page,{size: "A4",margin: 40},box("1. Helvetica (built-in): justified OK",{},[P]),box("2. Registered TTF (Arimo): NOT justified",{fontFamily: "Arimo"},[P]),box("3. Helvetica, mixed bold/italic runs: runs overlap",{},[B("Lorem ipsum")," dolor sit amet ",I("(consectetur adipiscing elit)")," sed do eiusmod tempor ",I("(incididunt ut labore et dolore)")," magna aliqua ut enim ad minim veniam."]),box("4. Helvetica, line ending with a forced break is stretched",{},["Lorem ipsum dolor sit amet\nConsectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."]),));writeFileSync("repro2.pdf",awaitrenderDocument(doc));console.log("ok");
Boxes 2, 3 and 4 of the resulting PDF show the three problems; box 1 is the reference. Thank you for Forme!
Thanks for the clear reproduction, all three reproduced and are fixed in #, which ships in the next release:
Registered fonts were justified only when the optimal line breaker found a solution. When it fell back to the greedy breaker (as in your paragraph), justification existed only as PDF word spacing (Tw), which doesn't apply to embedded TrueType fonts. Justification now goes into the glyph positions on every line.
The overlap was a regression in 0.26.0: each styled run started without the extra spacing of the run before it. Fixed.
A line ending with a forced break is no longer stretched, matching CSS.
Your five-box reproduction now renders all of them justified, with no overlap and the forced-break line left alone.
Activity
danmolitor commentedon Oct 6, 2026
Thanks for the clear reproduction, all three reproduced and are fixed in #, which ships in the next release:
Your five-box reproduction now renders all of them justified, with no overlap and the forced-break line left alone.
danmolitor commentedon Oct 6, 2026
Shipped in 0.28.0 (now on npm, PyPI and crates.io). See the 0.28.0 entry in CHANGELOG.md.