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.pyis 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_OVERPAINTis 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-onlygasptable worked much better than grid-fitting. - The current
gasptable 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 native0advance in thehmtxtable. The closing base glyph (qr_base_NNorqr_base_p55_NN) is the only glyph with a positiveADVANCEwidth. 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: nowrapordisplay: inline-blockto avoid breaks, as the font itself cannot override Unicode line-breaking properties.