pcbjam/tests/tools/screenshots/font8x8.ts
Viktor Vaczi 9787efc2c9 feat(tests): caption posted screenshots (name + spec); raise drift floor to 0.5%
Bake a caption strip onto every posted screenshot composite — status + name +
the spec that produces it — for changed, added, and removed; removed now shows
the old baseline image (was a text-only line). Zero native-dep: an embedded
public-domain 8x8 bitmap font (font8x8.ts) rendered by image-ops `withBottomLabel`;
the name→spec attribution is factored out of gen-manifest into a shared
`spec-map.ts` resolver. Bottom strip, colour per status (green/red/orange).
Applies in both compare (drift gate) and changelog (git-history diff), and
post-discord now attaches the captioned removed images.

Also raise the per-engine drift floor 0.2% → 0.5% (changedRatio) to absorb the
sub-1% inter-run flakiness seen after the re-baseline, while still catching real
localized changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:07:41 +02:00

123 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Minimal 8x8 bitmap font for baking captions onto screenshot composites.
*
* Glyphs are the public-domain `font8x8_basic` set (Daniel Hepper's font8x8,
* https://github.com/dhepper/font8x8 — public domain / "do whatever you want"),
* printable ASCII 0x200x7E, plus a custom middle-dot (·, 0xB7) used as the label
* separator. Public domain, so fine to embed under our GPLv3.
*
* Each glyph is 8 row-bytes (top→bottom). Within a row byte, bit `c` (LSB = 0) is
* column `c`, left→right: pixel (c,row) is set iff `(byte >> c) & 1`.
*/
// prettier-ignore
const GLYPHS: Record<string, number[]> = {
' ': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
'!': [0x18, 0x3c, 0x3c, 0x18, 0x18, 0x00, 0x18, 0x00],
'"': [0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
'#': [0x36, 0x36, 0x7f, 0x36, 0x7f, 0x36, 0x36, 0x00],
'$': [0x0c, 0x3e, 0x03, 0x1e, 0x30, 0x1f, 0x0c, 0x00],
'%': [0x00, 0x63, 0x33, 0x18, 0x0c, 0x66, 0x63, 0x00],
'&': [0x1c, 0x36, 0x1c, 0x6e, 0x3b, 0x33, 0x6e, 0x00],
"'": [0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00],
'(': [0x18, 0x0c, 0x06, 0x06, 0x06, 0x0c, 0x18, 0x00],
')': [0x06, 0x0c, 0x18, 0x18, 0x18, 0x0c, 0x06, 0x00],
'*': [0x00, 0x66, 0x3c, 0xff, 0x3c, 0x66, 0x00, 0x00],
'+': [0x00, 0x0c, 0x0c, 0x3f, 0x0c, 0x0c, 0x00, 0x00],
',': [0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c, 0x06],
'-': [0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00],
'.': [0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c, 0x00],
'/': [0x60, 0x30, 0x18, 0x0c, 0x06, 0x03, 0x01, 0x00],
'0': [0x3e, 0x63, 0x73, 0x7b, 0x6f, 0x67, 0x3e, 0x00],
'1': [0x0c, 0x0e, 0x0c, 0x0c, 0x0c, 0x0c, 0x3f, 0x00],
'2': [0x1e, 0x33, 0x30, 0x1c, 0x06, 0x33, 0x3f, 0x00],
'3': [0x1e, 0x33, 0x30, 0x1c, 0x30, 0x33, 0x1e, 0x00],
'4': [0x38, 0x3c, 0x36, 0x33, 0x7f, 0x30, 0x78, 0x00],
'5': [0x3f, 0x03, 0x1f, 0x30, 0x30, 0x33, 0x1e, 0x00],
'6': [0x1c, 0x06, 0x03, 0x1f, 0x33, 0x33, 0x1e, 0x00],
'7': [0x3f, 0x33, 0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x00],
'8': [0x1e, 0x33, 0x33, 0x1e, 0x33, 0x33, 0x1e, 0x00],
'9': [0x1e, 0x33, 0x33, 0x3e, 0x30, 0x18, 0x0e, 0x00],
':': [0x00, 0x0c, 0x0c, 0x00, 0x00, 0x0c, 0x0c, 0x00],
';': [0x00, 0x0c, 0x0c, 0x00, 0x00, 0x0c, 0x0c, 0x06],
'<': [0x18, 0x0c, 0x06, 0x03, 0x06, 0x0c, 0x18, 0x00],
'=': [0x00, 0x00, 0x3f, 0x00, 0x00, 0x3f, 0x00, 0x00],
'>': [0x06, 0x0c, 0x18, 0x30, 0x18, 0x0c, 0x06, 0x00],
'?': [0x1e, 0x33, 0x30, 0x18, 0x0c, 0x00, 0x0c, 0x00],
'@': [0x3e, 0x63, 0x7b, 0x7b, 0x7b, 0x03, 0x1e, 0x00],
'A': [0x0c, 0x1e, 0x33, 0x33, 0x3f, 0x33, 0x33, 0x00],
'B': [0x3f, 0x66, 0x66, 0x3e, 0x66, 0x66, 0x3f, 0x00],
'C': [0x3c, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3c, 0x00],
'D': [0x1f, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1f, 0x00],
'E': [0x7f, 0x46, 0x16, 0x1e, 0x16, 0x46, 0x7f, 0x00],
'F': [0x7f, 0x46, 0x16, 0x1e, 0x16, 0x06, 0x0f, 0x00],
'G': [0x3c, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7c, 0x00],
'H': [0x33, 0x33, 0x33, 0x3f, 0x33, 0x33, 0x33, 0x00],
'I': [0x1e, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x1e, 0x00],
'J': [0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1e, 0x00],
'K': [0x67, 0x66, 0x36, 0x1e, 0x36, 0x66, 0x67, 0x00],
'L': [0x0f, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7f, 0x00],
'M': [0x63, 0x77, 0x7f, 0x7f, 0x6b, 0x63, 0x63, 0x00],
'N': [0x63, 0x67, 0x6f, 0x7b, 0x73, 0x63, 0x63, 0x00],
'O': [0x1c, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1c, 0x00],
'P': [0x3f, 0x66, 0x66, 0x3e, 0x06, 0x06, 0x0f, 0x00],
'Q': [0x1e, 0x33, 0x33, 0x33, 0x3b, 0x1e, 0x38, 0x00],
'R': [0x3f, 0x66, 0x66, 0x3e, 0x36, 0x66, 0x67, 0x00],
'S': [0x1e, 0x33, 0x07, 0x0e, 0x38, 0x33, 0x1e, 0x00],
'T': [0x3f, 0x2d, 0x0c, 0x0c, 0x0c, 0x0c, 0x1e, 0x00],
'U': [0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3f, 0x00],
'V': [0x33, 0x33, 0x33, 0x33, 0x33, 0x1e, 0x0c, 0x00],
'W': [0x63, 0x63, 0x63, 0x6b, 0x7f, 0x77, 0x63, 0x00],
'X': [0x63, 0x63, 0x36, 0x1c, 0x1c, 0x36, 0x63, 0x00],
'Y': [0x33, 0x33, 0x33, 0x1e, 0x0c, 0x0c, 0x1e, 0x00],
'Z': [0x7f, 0x63, 0x31, 0x18, 0x4c, 0x66, 0x7f, 0x00],
'[': [0x1e, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1e, 0x00],
'\\': [0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x40, 0x00],
']': [0x1e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1e, 0x00],
'^': [0x08, 0x1c, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00],
'_': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff],
'`': [0x0c, 0x0c, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00],
'a': [0x00, 0x00, 0x1e, 0x30, 0x3e, 0x33, 0x6e, 0x00],
'b': [0x07, 0x06, 0x06, 0x3e, 0x66, 0x66, 0x3b, 0x00],
'c': [0x00, 0x00, 0x1e, 0x33, 0x03, 0x33, 0x1e, 0x00],
'd': [0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6e, 0x00],
'e': [0x00, 0x00, 0x1e, 0x33, 0x3f, 0x03, 0x1e, 0x00],
'f': [0x1c, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0f, 0x00],
'g': [0x00, 0x00, 0x6e, 0x33, 0x33, 0x3e, 0x30, 0x1f],
'h': [0x07, 0x06, 0x36, 0x6e, 0x66, 0x66, 0x67, 0x00],
'i': [0x0c, 0x00, 0x0e, 0x0c, 0x0c, 0x0c, 0x1e, 0x00],
'j': [0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1e],
'k': [0x07, 0x06, 0x66, 0x36, 0x1e, 0x36, 0x67, 0x00],
'l': [0x0e, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x1e, 0x00],
'm': [0x00, 0x00, 0x33, 0x7f, 0x7f, 0x6b, 0x63, 0x00],
'n': [0x00, 0x00, 0x1f, 0x33, 0x33, 0x33, 0x33, 0x00],
'o': [0x00, 0x00, 0x1e, 0x33, 0x33, 0x33, 0x1e, 0x00],
'p': [0x00, 0x00, 0x3b, 0x66, 0x66, 0x3e, 0x06, 0x0f],
'q': [0x00, 0x00, 0x6e, 0x33, 0x33, 0x3e, 0x30, 0x78],
'r': [0x00, 0x00, 0x3b, 0x6e, 0x66, 0x06, 0x0f, 0x00],
's': [0x00, 0x00, 0x3e, 0x03, 0x1e, 0x30, 0x1f, 0x00],
't': [0x08, 0x0c, 0x3e, 0x0c, 0x0c, 0x2c, 0x18, 0x00],
'u': [0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6e, 0x00],
'v': [0x00, 0x00, 0x33, 0x33, 0x33, 0x1e, 0x0c, 0x00],
'w': [0x00, 0x00, 0x63, 0x6b, 0x7f, 0x7f, 0x36, 0x00],
'x': [0x00, 0x00, 0x63, 0x36, 0x1c, 0x36, 0x63, 0x00],
'y': [0x00, 0x00, 0x33, 0x33, 0x33, 0x3e, 0x30, 0x1f],
'z': [0x00, 0x00, 0x3f, 0x19, 0x0c, 0x26, 0x3f, 0x00],
'{': [0x38, 0x0c, 0x0c, 0x07, 0x0c, 0x0c, 0x38, 0x00],
'|': [0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00],
'}': [0x07, 0x0c, 0x0c, 0x38, 0x0c, 0x0c, 0x07, 0x00],
'~': [0x6e, 0x3b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
'·': [0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00], // custom middle-dot separator
};
const BLANK = GLYPHS[' '];
/** 8 row-bytes for a character; falls back to a blank cell for anything unmapped. */
export function glyphFor(ch: string): number[] {
return GLYPHS[ch] ?? BLANK;
}
/** Whether a character has a real glyph (used for width estimation / fitting). */
export function hasGlyph(ch: string): boolean {
return ch in GLYPHS;
}