Repository navigation
Share the positioned-box equation and the text-decoration thickness lookup #767
Description
Two calculations in src/Mpdf.php are written out by hand wherever they are needed, and the copies have drifted apart. The first is the CSS 2.1 equation for a positioned box: left + margin-left + border-left + padding-left + width + padding-right + border-right + margin-right + right = containing width, and its vertical twin. WriteFixedPosHTML() has 31 copies of it. The second is the lookup of a text decoration's thickness and offset from the font's ut, up, strs, strp and CapHeight, with fallbacks. It has 5 copies across Text() and Cell(). Three bugs so far were a slip in one copy, and the copies in Text() still disagree with the ones in Cell().
Line numbers are on gravitypdf at 384d5b5.
Where.
The positioned-box equation, in WriteFixedPosHTML(). There are 16 vertical copies and 15 horizontal ones:
| Step | Vertical ($cont_h, $bbox_top/mt/bt/pt/pb/bb/mb/bottom, $inner_h) |
Horizontal ($cont_w, $bbox_left/ml/…/mr/right, $inner_w) |
|---|---|---|
Space left for min-width/max-width |
n/a | 15604 |
None auto, auto margins split |
15638 | 15684 |
None auto, one auto margin |
15641, 15643 | 15687, 15689 |
| Over-constrained, solve the end | 15645 | 15691 |
One of start, size and end auto |
15658, 15662, 15664, 15666 | 15705, 15709, 15711, 15713 |
| Inner box to draw | 15739 | 15742 |
| Temporary box for the first write | 15747, 15750 | 15754, 15757 |
| After the first write measures the content | 15824, 15828, 15835, 15837, 15841 | 15787, 15789 |
Border box handed to PaintDivBB() |
(via y1, 15987) |
15983 |
The static position is a short form of the same equation: $orig_y0 - $bbox_mt - $cont_y at 15624 and 15657, $orig_y0 - $cont_y at 15628, and $orig_x0 - $bbox_ml - $cont_x at 15680 and 15704.
The decoration thickness and offset:
| Copy | Thickness | Fallback | Offset |
|---|---|---|---|
Text() underline, 5366–5376 |
CurrentFont['ut'] × FontSize |
60/1000 | -up, fallback −100 |
Text() line-through, 5392–5402 |
CurrentFont['ut'] × FontSize |
60/1000 | CapHeight × 0.35 |
Cell() underline, 6179–6191 |
fonts[$key]['ut'] × size ÷ shrink |
60/1000 | -up + ut / 2 |
Cell() line-through, 6221–6242 |
strs, then ut |
50/1000 | strp, else CapHeight × $this->baselineS (0.3) |
Cell() overline, 6268–6281 |
ut of a font key cast to an integer |
60/1000 | CapHeight × $this->baselineO |
Each copy also repeats the colour switch and the w / 0 j 0 J / _dounderline() / restore sequence. Text() draws decorations for text in a text-rotate table cell (13429, 24065), for Write() (5476), for the watermark text (11759) and for SVG <text> (Image/Svg.php 2634).
Bugs it has caused.
WriteFixedPosHTML()subtracts the top margin twice, so a fixed or absolute block with amargin-topand notopgrows by its margin #737: five vertical copies (15645, 15658, 15662, 15664, 15666) subtract$bbox_mttwice and never subtract$bbox_mb. Fixed by Count each margin of a positioned block once when its top, height or bottom is solved (#737) #752.- A
min-widthwider than the container is capped at the container, andWriteFixedPosHTML()subtracts the left margin where it means the right #701: seven horizontal copies (15691, 15705, 15709, 15711, 15713, 15787, 15789) subtract$bbox_mltwice and never subtract$bbox_mr. Fixed by Let a block wider than its container stick out of it, and place a positioned block by both its margins (#701) #711. text-decoration: overlineignores the font's underline thickness, so it is drawn heavier than underline #741: the overline copy divides the font key by the shrink factor instead of the size. The key becomes0, soutis never found. Fixed by Draw an overline as thick as the font's underline (#741) #756.
Once #752, #711 and #756 are merged, every copy of the positioned-box equation has the same terms. The differences that are left are in behaviour, and each one may be a bug:
- Static position and the top margin. With
top,heightandbottomallautoand the margins not bothauto, 15628 places the margin edge at the static position, as CSS 2.1 does, so the margin pushes the box down. Withheightset (15657), and in both horizontal branches (15680, 15704), the margin is subtracted first, so the border edge sits at the static position and the margin does nothing. Amargin-topon a statically placed fixed block moves it or not depending on whether it has aheight. - Two auto margins that come out negative. 15684 splits the slack equally even when it is negative. §10.3.7 then sets
margin-leftto 0 (ormargin-rightunderrtl) and solves for the other. The vertical split at 15638 is correct as it is. - Over-constrained under
rtl. 15691 always solves forright. §10.3.7 ignoresleftwhen the containing block isrtl. - Only one axis gets each special case. The vertical axis has the centring rule that is not in CSS (15614–15625) and the clamp of a negative
topwhenoverflowis constrained (15826, 15839). The horizontal axis has themin-width/max-widthclamp (15600–15609). Neither axis has the other's. Text()decorations againstCell()ones. An underline in a rotated cell or an SVG<text>sitsut / 2higher than the same underline in a paragraph. Its line-through ignores the font'sstrs/strp, defaults to 60/1000 rather than 50/1000, and sits at 0.35 ×CapHeightrather thanbaselineS(0.3). It is drawn in the text colour rather than the decoration's colour, and an overline is not drawn at all (Text()has noFD_OVERLINEbranch).- Missing guards in
Cell(). The underline guardstextparam['u-decoration']withisset. The line-through and overline readtextparam['s-decoration']and['o-decoration']without one.
Fix. Write each calculation once.
Positioned box. Hold one axis as an array of its nine terms and solve it in one place:
/**
* One axis of the CSS 2.1 equation for an absolutely positioned, non-replaced box (§10.3.7, §10.6.4): the term
* $unknown is what is left of $containing once the other eight are taken away
*
* @param float $containing The containing block's width or height
* @param array $terms start, margin_start, border_start, padding_start, size, padding_end, border_end, margin_end, end
* @param string $unknown The key of the term to solve for
*
* @return float
*/
private function solvePositionedAxis($containing, array $terms, $unknown)Build $horizontal and $vertical from the $bbox_* values once, after 15565, and replace the 31 hand-written lines with calls. Then lift the auto cases (15612–15715), which have the same shape on both axes, into one resolvePositionedAxis($containing, array $terms, $staticStart, $endIsOverConstrained) that returns the terms with every auto filled. Call it twice. Pass in the parts that belong to one axis, such as the over-constrained side for rtl, the centring rule and the WidthConstraints clamp, rather than branching on the axis inside. This could go in a small Mpdf\Css\PositionedAxis class instead, which would be easier to unit test than a private method on Mpdf.
Decorations. One method returns the line for a decoration, given the font's metrics:
/**
* The thickness of a text decoration line and its offset from the baseline, in user units, from the font's
* underline or strikeout metrics where it has them
*
* @param array $font An entry of $this->fonts, or [] when the decoration's font is not known
* @param float $fontSize The decoration's font size, already divided by any shrink factor
* @param int $decoration TextVars::FD_UNDERLINE, FD_LINETHROUGH or FD_OVERLINE
*
* @return float[] The thickness and the offset
*/
private function decorationLine(array $font, $fontSize, $decoration)Cell() passes $this->fonts[$decorationfontkey] and the shrunk size. Text() passes $this->CurrentFont and $this->FontSize. A second helper could draw the line itself (colour, w 0 j 0 J, _dounderline(), restore), so that the three Cell() blocks and the two Text() blocks are each one call.
Land this after #752, #711 and #756, keeping output byte for byte. Decide the six divergences above one at a time, each with its own test and changelog entry, rather than folding them into the refactor. Once both paths share the helper, item 5 is a one-line choice.
Tests.
- Unit tests for
solvePositionedAxis()/resolvePositionedAxis()covering eachautocase of §10.3.7 and §10.6.4, with unequal start and end margins, borders and padding, so that a swapped term fails. - Unit tests for
decorationLine(): a font withut/up/strs/strp, one without, a core font, and a size divided by a shrink factor. - The full suite and the snapshot suite pass unchanged, in particular
PositionedBlockVerticalMarginsTest(Count each margin of a positioned block once when its top, height or bottom is solved (#737) #752),BlockWidthOverflowTest(Let a block wider than its container stick out of it, and place a positioned block by both its margins (#701) #711) andOverlineThicknessTest(Draw an overline as thick as the font's underline (#741) #756).
Found while working on #552.
🤖 Generated with Claude Code
Activity