mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-08-04 15:12:12 +00:00
fix(pdf): make dompdf honour declared line-heights (#736)
dompdf does not use a declared line-height directly. It scales it by the font's
own height:
rendered = declared x (ascent + descent) / unitsPerEm x font_height_ratio
The bundled Noto Sans reports 1.362 for that middle term, so at dompdf's stock
font_height_ratio of 1.1 every line-height in every document came out 1.4985x
what the CSS asked for. Chromium honours the declared value exactly. That one
factor was the whole vertical disagreement between the two drivers.
Setting the ratio to 1/1.362 cancels the font term. Measured on a declared 15px
(11.25pt): 16.86pt at the stock 1.1, 15.32pt at 1.0, and 11.25pt at this value --
identical to Chromium.
Across the seven document templates the worst-edge ink difference falls from
roughly 70-150pt to under 30pt on six of them, and to 3.4pt on invoice1. The
exception is estimate1, which moves the other way: with the line-height noise
gone, a float and padding difference in its address block is now the dominant
term there. That is a separate problem this exposes rather than causes.
Worth recording that an earlier compensation shim had arrived at 1.5 empirically
and was right: 1.1 x 1.362 = 1.4985. I argued against it on the strength of a
test that used font-family: sans-serif, which resolves to a built-in core font
and so never exercised the embedded Noto Sans path where the scaling happens.
The measurement was wrong, not the constant.
PdfLineHeightTest pins the invariant -- the font's reported height equals the
font size, so a declared length renders at that length -- and needs no Gotenberg,
so CI holds it. Swapping the default face or taking a dompdf upgrade that changes
the computation now fails a test rather than quietly reintroducing the drift.
Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
This commit is contained in:
committed by
GitHub
parent
bd9602b130
commit
fda75e9af7
@@ -245,8 +245,25 @@ return [
|
||||
|
||||
/**
|
||||
* A ratio applied to the fonts height to be more like browsers' line height
|
||||
*
|
||||
* dompdf does not use a declared `line-height` directly: it scales it by
|
||||
* the font's own height, so the rendered line box is
|
||||
*
|
||||
* declared × (ascent + descent) / unitsPerEm × font_height_ratio
|
||||
*
|
||||
* The bundled Noto Sans reports 1.362 for that middle term, so at the
|
||||
* stock 1.1 every line-height in every PDF came out 1.4985× what the CSS
|
||||
* asked for. Chromium honours the declared value exactly, which is where
|
||||
* the two drivers disagreed vertically on every document.
|
||||
*
|
||||
* 1 / 1.362 cancels the font term, so a declared line-height means what
|
||||
* it says and both drivers agree. Measured: a declared 15px (11.25pt)
|
||||
* renders at 16.86pt here at 1.1, 15.32pt at 1.0, and 11.25pt at this
|
||||
* value -- identical to Chromium. Calibrated against Noto Sans, the
|
||||
* default face; PdfLineHeightTest pins it so a font or dompdf upgrade
|
||||
* that moves it fails rather than drifting.
|
||||
*/
|
||||
'font_height_ratio' => 1.1,
|
||||
'font_height_ratio' => 0.7342,
|
||||
|
||||
/**
|
||||
* Use the more-than-experimental HTML5 Lib parser
|
||||
|
||||
53
tests/Unit/PdfLineHeightTest.php
Normal file
53
tests/Unit/PdfLineHeightTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* dompdf does not use a declared `line-height` directly. It scales it by the
|
||||
* font's own height:
|
||||
*
|
||||
* rendered = declared × (ascent + descent) / unitsPerEm × font_height_ratio
|
||||
*
|
||||
* The bundled Noto Sans reports 1.362 for that middle term, so at dompdf's stock
|
||||
* font_height_ratio of 1.1 every line-height in every document came out 1.4985×
|
||||
* what the CSS asked for. Chromium honours the declared value exactly, and that
|
||||
* single factor was the whole vertical disagreement between the two drivers --
|
||||
* measured at ~100pt across a page before this, and under 10pt on most templates
|
||||
* after.
|
||||
*
|
||||
* config/dompdf.php sets the ratio to 1/1.362 to cancel the font term. The
|
||||
* invariant that buys is simply: the font's reported height equals the font
|
||||
* size, so a declared length is rendered at that length.
|
||||
*
|
||||
* This is calibrated against Noto Sans. Swap the default face, or take a dompdf
|
||||
* upgrade that changes the computation, and this fails rather than quietly
|
||||
* reintroducing the drift.
|
||||
*/
|
||||
function notoSansHeightAt(float $size): float
|
||||
{
|
||||
$metrics = app('dompdf.wrapper')->getDomPDF()->getFontMetrics();
|
||||
$font = $metrics->getFont('NotoSans', 'normal');
|
||||
|
||||
expect($font)->not->toBeFalsy('NotoSans should resolve; it is the bundled default face');
|
||||
|
||||
return $metrics->getFontHeight($font, $size);
|
||||
}
|
||||
|
||||
test('a declared line-height is rendered at its declared size', function (float $size) {
|
||||
expect(notoSansHeightAt($size))->toEqualWithDelta($size, 0.01);
|
||||
})->with([9.0, 12.0, 15.0, 18.0, 24.0]);
|
||||
|
||||
test('the configured ratio is the one that cancels the font metrics', function () {
|
||||
// Guards the value itself, so a well-meaning "restore the dompdf default"
|
||||
// shows up as a failing test rather than as documents silently growing.
|
||||
expect(config('dompdf.defines.font_height_ratio'))->toEqualWithDelta(0.7342, 0.0005);
|
||||
});
|
||||
|
||||
/**
|
||||
* The stock ratio is what the drift looked like: 1.1 × 1.362 ≈ 1.4985, which is
|
||||
* within a rounding error of the 1.5 that a hand-tuned compensation shim had
|
||||
* arrived at empirically.
|
||||
*/
|
||||
test('dompdf stock ratio would inflate every line by about half again', function () {
|
||||
$natural = notoSansHeightAt(12.0) / config('dompdf.defines.font_height_ratio');
|
||||
|
||||
expect($natural * 1.1 / 12.0)->toEqualWithDelta(1.4985, 0.005);
|
||||
});
|
||||
Reference in New Issue
Block a user