templates/lib.typ gains WCAG luminance/contrast-ratio/ink-on and a surface-aware palette-of(). framed and split place type on flat colour and were inheriting an ink measured against the artwork, rendering white on cream at 1.15:1. Now computed per surface: 18.34:1 on the regression fixture. Accent held to the same bar with fallback to ink. Adds tests/contrast-check.typ, run per fixture by the harness. 35/35 render combos and 7/7 contrast assertions pass.
4.2 KiB
Defects found by the fixture harness
1. BLOCKER FIXED — ink colour ignored what was actually behind the text
Symptom. In framed and on the flat panel of split, body text renders white on the
cream palette.bg (#f4efe6) and is essentially illegible. Reproduce with:
tests/render-fixtures.sh /path/to/typst
# then look at tests/out/framed__accents.png and split__landscape.png
Cause. templates/lib.typ:61, in palette-of():
let ink = hex(_get(spec, "ink_resolved", _get(p, "ink", none)), fallback: rgb("#111111"))
ink_resolved always wins over palette.ink. But ink_resolved is measured by
render/contrast.ts against the artwork. That is correct only for text sitting over
the artwork. Templates that place text on flat colour — framed entirely, split on its
colour half — inherit an ink chosen for a completely different surface. split.typ:7
already carries a comment noticing the tension.
Fixed. Contrast is now resolved per surface, computed rather than declared.
Measured on tests/fixtures/surface-contrast.json (dark artwork + cream background):
| ink-on-background contrast | |
|---|---|
| before | 1.15 : 1 — unreadable (WCAG large-text minimum is 3.0) |
| after | 18.34 : 1 |
templates/lib.typ gained real WCAG machinery — luminance(), contrast-ratio(),
ink-on() and MIN-CONTRAST — and palette-of(spec, surface: ...) now takes the
surface the text will sit on:
auto/"art"— over artwork, use the renderer's measured ink (unchanged behaviour, sohero-bottomandcentred-stackare untouched);"bg"— on the flat page background;- a colour — on that exact colour, for a band or a panel.
block-style() and block-text() take the same surface: argument. framed and
split — the two templates whose type sits on flat colour, and the only two that were
broken — now pass surface: "bg". The accent colour is held to the same bar and falls
back to the ink when it fails, so an unreadable accent can no longer ship.
banded needed no change: it already adapted its band fill to the ink by contrast.
Guarded by tests/contrast-check.typ, run for every fixture by the harness. It asserts
the ink, the accent, and an arbitrary panel colour all clear 3.0:1. A negative control
confirms the check bites rather than passing vacuously.
Original analysis:
- Extend
ResolvedSpecwithink_on_art(measured, today'sink_resolved) andink_on_bg(checked againstpalette.bg;design/palettes.tsalready guarantees every palette meets 4.5:1, so this is normally justpalette.ink). - Add
ink-for(spec, surface)tolib.typ, where surface is"art"or"bg", and have each template ask for the surface its text actually sits on. - Keep
ink_resolvedas a deprecated alias so nothing breaks mid-migration. resolveSpec()inrender/typst.tsmust populate both.
Add a fixture whose artwork is dark and whose palette.bg is light — the case where a
single ink cannot possibly satisfy both surfaces — so this cannot regress silently.
2. Gotcha (fixed in the harness, must hold in render/typst.ts)
Typst resolves a leading / against --root, not the filesystem. Passing an absolute
path to --input specfile=... yields <root>/home/you/... → "file not found". The spec
path must be root-relative. Same rule applies to art_file and every font_files
entry.
Confirmed working
- A3 trim geometry exact: TrimBox 297.0 × 420.0 mm inside a 303 × 426 mm MediaBox.
- Italian typography:
PERCHÉ,È COSÌ,CITTÀ,SANT'ANNA,FORLÌ,«PIAZZA GRANDE». - Auto-fit: a 92-character title shrinks and wraps rather than overflowing.
splitgenuinely adapts to landscape (art left / text right) rather than assuming portrait.- 18/18 template × fixture combinations compile.
Regression fixture for defect 1
tests/fixtures/surface-contrast.json pairs dark artwork (/tests/art-dark.png,
mean luminance ≈ 0.05) with a light background (#f4efe6) and sets
ink_resolved: "#ffffff". No single ink can satisfy both surfaces, so any template that
puts text on flat colour while using the art-measured ink fails visibly. Once the fix
lands, every template must stay legible on this fixture.