0
0
Files
qr-font/AGENTS.md

5.1 KiB

AGENTS.md

Context for future agents working on this repo.

Project

This project generates an experimental OpenType font that renders bracketed text as QR Code symbols while keeping surrounding text readable. The generated font is a Modified Version of Liberation Sans Regular and is licensed under the SIL Open Font License 1.1.

The bounded QR targets are:

  • QR Code Version 1-L (up to 17 characters per block)
  • QR Code Version 2-L (up to 32 characters per block)
  • QR Code Version 3-L (up to 53 characters per block)
  • byte mode
  • fixed mask pattern 0
  • [ and ] delimiters

Example:

Hello [QR coded] world!
Download this font: [http://qr.jim.sh/]

Build

Use uv for Python commands and dependency management.

make

make runs the full Reed-Solomon parity build and compiles the fonts and index.html demo into the dist/ directory.

Use make deploy to synchronize the built dist/ assets directly to psy:/www/qr/ (which cleans up stale/experimental remote files via --delete).

Do not use the placeholder parity path for normal verification. It exists only as a legacy layout-debug target:

make fast-placeholder

The generator defaults to:

/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf

Use --base-font to test another compatible TrueType base font.

Important Implementation Details

  • tools/build_font.py is the source of truth. It emits glyph outlines, OpenType feature code, HTML demo, and SVG reference output.
  • Printable ASCII glyphs are copied from Liberation Sans and scaled down with LATIN_SCALE, so normal text can surround QR blocks in the same font.
  • Latin advances are snapped to 100 font-unit increments. This helps QR blocks start on the QR module grid after ordinary text.
  • QR modules are drawn as slightly overpainted rectangles. Current MODULE_OVERPAINT is intentional: it hides faint gray seams between QR helper glyph layers.
  • QR helper glyphs carry a tiny no-op TrueType program (PUSHB[1] 0, POP[]). This was added after Chrome/Firefox showed one-pixel offsets between adjacent rectangles. The no-op program plus smoothing-only gasp table worked much better than grid-fitting.
  • The current gasp table intentionally avoids grid-fit flags and uses grayscale/symmetric smoothing only.

Verification

Lightweight checks:

uv run tools/shape_debug.py 'Hello [QR coded] world!' 'Download this font: [http://qr.jim.sh/]'

For QR matrix correctness, compare matrix_for_text() against an independent QR encoder in byte mode, version 1, level L, mask 0. Previous checks used:

uv run --with qrcode[pil] python ...

Browser rendering is useful but time-consuming. The user asked to skip routine Firefox rendering for now. If visual alignment regresses, inspect recent screenshots in ~/Downloads.

We build three separate font families (1-L, 2-L, 3-L), each fixed to its respective QR version. Supporting multiple QR versions dynamically in a single font file is possible, but would require branching by payload length at close-delimiter time and emitting version-specific base patterns, coordinate maps, RS parity circuits, and advances. The current strategy compiles separate fonts for each version.

Browser Layout, Alignment, and Line-Breaking Details

Firefox Alignment Issue

  • Symptom: Horizontal shifting/slicing between the top/bottom (parity) and middle (data) sections of the QR code when resizing the font in Firefox.
  • Cause: Parity/state glyphs were classified as GDEF Marks and had zero-advance in hmtx. Firefox applies subpixel snapping/rounding to zero-advance GDEF Marks differently from standard spacing Base glyphs, causing horizontal coordinate drift.
  • Solution: We omit the GDEF table and configure all intermediate QR-related glyphs (header_bits, byte_XX, pXX, sXX) to have native 0 advance in the hmtx table. The closing base glyph (qr_base_NN or qr_base_p55_NN) is the only glyph with a positive ADVANCE width. Because there are no GPOS positioning adjustments, the browser treats them all as zero-advance Base glyphs, eliminating horizontal misalignment entirely.

Line-Breaking Limitations

  • Symptom: QR codes containing spaces, dots, or slashes split across lines. In Chrome, the second half of the split QR code renders as plain text (e.g., coded]). In Firefox, it splits the shaped QR code.
  • Cause: Web browsers run line-breaking algorithms (Unicode UAX #14) on the raw Unicode text BEFORE they run the font shaper (HarfBuzz). Because line-breaking is decided on Unicode characters before GSUB/GPOS run, font-level ligatures designed to ligate breaking characters to their neighbors cannot prevent browser wrapping.
  • Chrome Behavior: Chrome splits the string into two independent lines and shapes them separately. Since the second line lacks [, it remains plain text.
  • Firefox Behavior: Firefox shapes first and then splits the shaped glyph run.
  • Rule of Thumb: Always wrap QR-coded elements in a CSS container with white-space: nowrap or display: inline-block to avoid breaks, as the font itself cannot override Unicode line-breaking properties.