Skip to content

Read the text-decoration longhands and text-align start, end and match-parent (#607) - #722

Open
gravitypdf-bot wants to merge 2 commits into
gravitypdffrom
feature/607-text-decoration-align-word-break
Open

gravitypdf-bot wants to merge 2 commits into
gravitypdffrom
feature/607-text-decoration-align-word-break

Conversation

@gravitypdf-bot

Copy link
Copy Markdown

Part of #607
Part of #552, part of #520

Problem

mPDF read only the lines of text-decoration, matching underline, overline and line-through inside it by substring. So the longhands, the style, colour and thickness parts of the shorthand, text-underline-offset and text-underline-position were all ignored, and every decoration was drawn as a solid line in the text colour. text-align: start and end, which Bootstrap 5 uses for .text-start and .text-end, and match-parent matched none of the keywords setCSS() or Tag::ALIGN knew.

$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML('<p style="text-decoration: underline wavy #f00">Wavy</p>
<div style="width: 100mm; text-align: end">End</div>
<table style="width: 100mm"><tr><td style="text-align: end">Cell</td></tr></table>');
$mpdf->Output('', 'S');
Rule Browser Before After
p { text-decoration-line: underline } underlined no line underlined
p { text-decoration: underline wavy #f00 } red wavy underline straight underline in the text colour red wavy underline
text-decoration: underline double / dotted / dashed that style solid that style
text-decoration-thickness: 2pt 2pt line the font's thickness 2pt line
text-underline-offset: 6pt 6pt below the baseline the font's position 6pt below the baseline
text-underline-position: under below the descenders the font's position below the descenders
div { width: 100mm; text-align: end } right-aligned left-aligned right-aligned
div { width: 100mm; direction: rtl; text-align: end } left-aligned right-aligned left-aligned
td { text-align: end } right-aligned left-aligned right-aligned
<div style="text-align: end"><p dir=rtl style="text-align: match-parent"> right-aligned right (rtl default) right-aligned

Fix

Everything here is gated on CssMode::STANDARD.

Decorations

  • NormalizeProperties expands the text-decoration shorthand: the line keywords stay in TEXT-DECORATION, so the propagation from Draw an ancestor's text decorations over its descendants instead of letting them inherit or undo them #654 and everything else that reads it is unchanged. The style, colour (currentcolor by default) and thickness go to their longhands, each reset to its initial value where the shorthand leaves it out. text-decoration-line is read as TEXT-DECORATION. A CSS-wide keyword on the shorthand reaches each longhand. CssWideKeywords knows the new initial values.
  • setCSS() adds the element's style, decoration colour, thickness, underline offset and under position to the text parameter of each line the element sets itself (Mpdf::decorationDrawing()). Because Draw an ancestor's text decorations over its descendants instead of letting them inherit or undo them #654 copies those parameters to descendants, a propagated line keeps its own style and colour. An element's own decoration drops the keys of the one it replaces, so <span style="text-decoration: underline"> inside a wavy paragraph draws a solid line.
  • The drawing code takes the thickness from the parameter, and for the underline places its top at the offset below the baseline, or below the font's descent for under. TextDecorations::stroke() draws solid, double (two lines a thickness apart, the second away from the text; either side of the line for line-through), dotted (round dots), dashed, and wavy (whole half waves of a sine, each one cubic Bézier, fitted to the text width).
  • text-underline-offset and text-underline-position are inherited (InheritedProperties::DECORATION) and read where the underline is set, so p { text-underline-offset: 2mm } moves the underline of a link inside it. A length in em is taken of the element that declares it, like other inherited lengths.

Alignment

  • start and end map to left or right against the block's direction after setCSS() has read it, and against the cell's direction in Td.
  • match-parent is resolved in ComputedValues: it computes to the parent's alignment, with the parent's start or end taken against the parent's direction, so its children inherit the resolved value.

Decisions

  • Offset. A length offset is from the alphabetic baseline to the top of the line, as Chrome and Firefox draw it, so text-underline-offset: 0 puts the line at the baseline. auto keeps the font's position.
  • Thickness. auto and from-font both take the font's post underline thickness, which is what mPDF already drew. A percentage is of 1em.
  • text-underline-position: left / right are for vertical text, which mPDF does not set, so they are read as auto.
  • Wavy proportions. Crest to trough is three thicknesses and a half wave three thicknesses long, near what browsers draw.

Legacy mode

Unchanged. The shorthand is not expanded, the longhands are not read, and start, end and match-parent are ignored, as before. Solid lines are drawn through the same operators as before, and no existing snapshot fixture changed. Nothing here only fixes a crash, so nothing applies in both modes.

Not in this PR

Tests

  • tests/Mpdf/TextDecorationLonghandsTest.php: the line longhand; the shorthand's parts in any order; legacy drawing a plain line in the text colour; the shorthand and longhands cascading both ways; the colour on a nested span and on a propagated line; an own decoration not keeping the style it replaces; the operators each of the five styles writes; double-line spacing for underline and line-through; a wavy line spanning the text and alternating; thickness as a length, a percentage and on line-through/overline, with auto/from-font unchanged; the offset from the baseline in pt and em; under below the descenders with an offset from there, using a font with descenders; inheritance of the offset to an inline element, a child block and a cell; CSS-wide keywords on the longhands; legacy ignoring all of it byte for byte.
  • tests/Mpdf/TextAlignLogicalTest.php: start/end in ltr and rtl blocks and cells against their physical equivalents, end inherited into an rtl child, match-parent against the parent's end and rtl start, a physical parent value, its resolved value handed on, a cell's match-parent from its row, a cell inheriting end from its table, and legacy ignoring them.
  • Snapshot text-decoration-align: each style and colour, thickness, the font's underline position beside text-underline-offset: 0, 6pt and text-underline-position: under, then start/end in ltr and rtl blocks, match-parent, and end in ltr and rtl cells, each under a caption saying what it should show. I rasterised it and checked each case against its caption. Against the pre-fix src/ it fails (page 1 RMSE 0.039).

Checks

🤖 Generated with Claude Code

gravitypdf-bot and others added 2 commits October 12, 2026 07:48
…h-parent (#607)

mPDF read only the lines of text-decoration and matched them by substring,
so the style, colour and thickness of the shorthand, the longhands and
text-underline-offset/-position were ignored, and every decoration was a
solid line in the text colour. text-align: start and end matched none of
the keywords mPDF knows, so end left ltr text at the left.

Under the standard cssMode the shorthand now expands into its longhands
(TEXT-DECORATION keeps the lines, so the propagation from #654 reads them
as before), each decoration keeps its own style, colour, thickness and
underline position, and the line is stroked solid, double, dotted, dashed
or wavy. start and end are taken against the block's or cell's direction,
and match-parent computes to the parent's alignment. Legacy mode is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yle through one path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant