Skip to content

Share the positioned-box equation and the text-decoration thickness lookup #767

Open

Description

@gravitypdf-bot

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.

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:

  1. Static position and the top margin. With top, height and bottom all auto and the margins not both auto, 15628 places the margin edge at the static position, as CSS 2.1 does, so the margin pushes the box down. With height set (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. A margin-top on a statically placed fixed block moves it or not depending on whether it has a height.
  2. Two auto margins that come out negative. 15684 splits the slack equally even when it is negative. §10.3.7 then sets margin-left to 0 (or margin-right under rtl) and solves for the other. The vertical split at 15638 is correct as it is.
  3. Over-constrained under rtl. 15691 always solves for right. §10.3.7 ignores left when the containing block is rtl.
  4. 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 top when overflow is constrained (15826, 15839). The horizontal axis has the min-width/max-width clamp (15600–15609). Neither axis has the other's.
  5. Text() decorations against Cell() ones. An underline in a rotated cell or an SVG <text> sits ut / 2 higher than the same underline in a paragraph. Its line-through ignores the font's strs/strp, defaults to 60/1000 rather than 50/1000, and sits at 0.35 × CapHeight rather than baselineS (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 no FD_OVERLINE branch).
  6. Missing guards in Cell(). The underline guards textparam['u-decoration'] with isset. The line-through and overline read textparam['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.

Found while working on #552.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions