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
Paper size was a Gotenberg-only setting stored as a single "210mm 297mm"
string. dompdf had no page settings at all: size was pinned to config/dompdf.php's
fixed 'a4', its top-level `orientation` key was read by nothing (the installed
barryvdh v3 builds options only from `defines`), and margins were whatever
dompdf's own stylesheet said. So the two drivers disagreed about margins by
default -- dompdf 1.2cm, Gotenberg hardcoded to zero -- and selecting dompdf
silently discarded the paper size.
Replaces gotenberg_papersize with pdf_paper_width / pdf_paper_height /
pdf_orientation / pdf_margin_{top,right,bottom,left}, saved and applied for
either driver. Width and height are separate CSS lengths because that is the
only lossless shared notation: Gotenberg has no named sizes, and dompdf's named
table cannot express everything Gotenberg accepts. Named presets (A3/A4/A5/
Letter/Legal) are a convenience in the UI that resolve to a pair of lengths.
PdfPageSetup resolves it once and translates: a points array plus an orientation
argument for dompdf, CSS lengths plus landscape() for Gotenberg. Both are handed
the portrait pair, since each swaps the axes itself. Gotenberg's margins() takes
top, bottom, left, right, which is not the CSS order.
dompdf exposes no margin API, so DompdfDriver injects an @page rule -- at the
top of <head>, so a template declaring its own still wins. Doing it in the driver
rather than a Blade partial means custom templates get it without including
anything.
Margins default to 1.2cm, dompdf's existing default, so Gotenberg starts
matching it rather than rendering edge-to-edge. Verified against a live
gotenberg:8: A4 portrait, A4 landscape and Letter at zero margins all come out
with the same page box and the same ink offsets on both drivers.
A malformed length now throws rather than being ignored. Blank still falls back,
but a value that is set and wrong is an operator mistake, and the drivers would
otherwise fail differently: dompdf throws converting to points, Gotenberg would
forward the string and render at some other size.
Also here:
- Migration splits an existing gotenberg_papersize into the new pair. It earns
its place because that key ships in 2.x, not just a 3.x alpha, so a stable
install that chose Letter would otherwise come back up on A4. Drops
gotenberg_margins, which 2.x also stores and neither driver ever read.
- Removes EnvironmentManager::savePDFVariables/getPDFConfiguration, which had no
caller anywhere, and the unused EnvironmentManager injection in the controller.
- config/dompdf.php: drops the dead `orientation` key and defaults enable_remote
to false, matching .env.example, which sets it explicitly and explains why.
Installs predating that line were falling back to true.
- Retires the settings.pdf.footer_text and pdf_layout strings, which no component
referenced.
Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
Bundle Noto Sans (Regular/Bold/Italic/BoldItalic) under resources/static/fonts/ as the default PDF face — it covers Latin, Cyrillic, Greek, Arabic, Thai and Hindi out of the box, replacing the limited DejaVu Sans fallback. Move all @font-face declarations into app.pdf.partials.fonts and include it from every invoice/estimate/payment/report template, dropping per-template font-family hardcodes and the conditional Thai locale include.
Introduce FontService + FontController to download static Noto Sans CJK packages (zh, zh_CN, ja, ko) from life888888/cjk-fonts-ttf on demand. GeneratesPdfTrait::ensureFontsForLocale primes the family before rendering and the partial emits @font-face rules for installed packages so dompdf resolves them through standard CSS — no separate registerFont() instance required. Static TTFs are mandatory because dompdf's PHP-Font-Lib does not parse variable fonts (fvar/gvar tables), which is why Google Fonts' NotoSansTC[wght].ttf rendered empty boxes.
Expose status/install via /api/v1/fonts/status and /api/v1/fonts/{package}/install with matching FONTS_STATUS / FONTS_INSTALL constants in scripts-v2/api/endpoints.ts. Flip DOMPDF_ENABLE_REMOTE default to true for remote asset loading.
* Create PHP CS Fixer config and add to CI workflow
* Run php cs fixer on project
* Add newline at end of file
* Update to use PHP CS Fixer v3
* Run v3 config on project
* Run seperate config in CI