Files
pi-imgen/docs/OPEN-DEFECTS.md
T
mozempk 5d27417c54 fix(render): resolve ink contrast per surface, not once per poster
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.
2026-08-27 09:33:08 +02:00

4.2 KiB
Raw Blame History

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, so hero-bottom and centred-stack are 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 ResolvedSpec with ink_on_art (measured, today's ink_resolved) and ink_on_bg (checked against palette.bg; design/palettes.ts already guarantees every palette meets 4.5:1, so this is normally just palette.ink).
  • Add ink-for(spec, surface) to lib.typ, where surface is "art" or "bg", and have each template ask for the surface its text actually sits on.
  • Keep ink_resolved as a deprecated alias so nothing breaks mid-migration.
  • resolveSpec() in render/typst.ts must 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.
  • split genuinely 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.