internal/site/bdf.go
Ref: Size: 6.0 KiB History
// bdf.go reads glyph bitmaps out of a BDF bitmap font. The site draws its
// mark and link-preview cards from real font pixels rather than hand-traced
// paths, so the two stay the same shape; this reader covers only what that
// needs — per-rune ink and advances.
//
// The embedded fonts are GohuFont (font.gohu.org), copyright Hugo Chargois,
// licensed under the WTFPL. Their own COMMENT lines carry that notice.
package site
import (
"bufio"
"bytes"
_ "embed"
"encoding/hex"
"fmt"
"strconv"
"strings"
)
//go:embed gohufont-11.bdf
var gohu11 []byte
//go:embed gohufont-14b.bdf
var gohu14b []byte
// bdfGlyph is one glyph's ink box. pix holds h rows of w pixels, top row
// first; xoff/yoff place that box against the drawing origin, with yoff
// measured from the baseline (negative reaches below it).
type bdfGlyph struct {
w, h, xoff, yoff int
advance int // DWIDTH: pen movement to the next glyph
pix [][]bool
}
// bdfFont is a parsed font: glyphs by rune, plus the line metrics every
// caller needs to stack lines without knowing the format.
type bdfFont struct {
name string
glyphs map[rune]bdfGlyph
ascent, descent int
}
// lineHeight is the full line box: rows [0,ascent) sit above the baseline,
// the rest below.
func (f *bdfFont) lineHeight() int { return f.ascent + f.descent }
// textRun is a horizontal run of ink, in font pixels from the top-left of
// the line box. Runs — not single pixels — because both outputs draw
// rectangles, and a run is one rect instead of many.
type textRun struct{ X, Y, W int }
// parseBDF reads a BDF font. Fonts are inputs to the build, so anything
// malformed or incomplete is an error rather than a silently blank glyph.
func parseBDF(src []byte) (*bdfFont, error) {
f := &bdfFont{glyphs: map[rune]bdfGlyph{}}
var (
cur bdfGlyph
code = -1
haveBBX bool
inBitmap bool
rows []string
ascentOK bool
descOK bool
)
sc := bufio.NewScanner(bytes.NewReader(src))
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for line := 1; sc.Scan(); line++ {
text := strings.TrimRight(sc.Text(), "\r")
key, rest, _ := strings.Cut(text, " ")
if inBitmap && key != "ENDCHAR" {
rows = append(rows, strings.TrimSpace(text))
continue
}
switch key {
case "FONT":
f.name = strings.TrimSpace(rest)
case "FONT_ASCENT", "FONT_DESCENT":
n, err := strconv.Atoi(strings.TrimSpace(rest))
if err != nil {
return nil, fmt.Errorf("line %d: %s: %w", line, key, err)
}
if key == "FONT_ASCENT" {
f.ascent, ascentOK = n, true
} else {
f.descent, descOK = n, true
}
case "STARTCHAR":
cur, code, haveBBX, rows = bdfGlyph{}, -1, false, nil
case "ENCODING":
n, err := strconv.Atoi(strings.TrimSpace(rest))
if err != nil {
return nil, fmt.Errorf("line %d: ENCODING: %w", line, err)
}
code = n
case "DWIDTH":
fields := strings.Fields(rest)
if len(fields) == 0 {
return nil, fmt.Errorf("line %d: DWIDTH: no value", line)
}
n, err := strconv.Atoi(fields[0])
if err != nil {
return nil, fmt.Errorf("line %d: DWIDTH: %w", line, err)
}
cur.advance = n
case "BBX":
fields := strings.Fields(rest)
if len(fields) != 4 {
return nil, fmt.Errorf("line %d: BBX: want 4 values, got %d", line, len(fields))
}
vals := make([]int, 4)
for i, s := range fields {
n, err := strconv.Atoi(s)
if err != nil {
return nil, fmt.Errorf("line %d: BBX: %w", line, err)
}
vals[i] = n
}
cur.w, cur.h, cur.xoff, cur.yoff = vals[0], vals[1], vals[2], vals[3]
haveBBX = true
case "BITMAP":
if !haveBBX {
return nil, fmt.Errorf("line %d: BITMAP before BBX", line)
}
inBitmap = true
case "ENDCHAR":
inBitmap = false
if code < 0 {
return nil, fmt.Errorf("line %d: glyph without ENCODING", line)
}
pix, err := decodeBitmap(rows, cur.w, cur.h)
if err != nil {
return nil, fmt.Errorf("line %d: glyph %d: %w", line, code, err)
}
cur.pix = pix
// ISO8859-1 code points are their own runes.
f.glyphs[rune(code)] = cur
}
}
if err := sc.Err(); err != nil {
return nil, err
}
switch {
case !ascentOK || !descOK:
return nil, fmt.Errorf("font %q: missing FONT_ASCENT/FONT_DESCENT", f.name)
case len(f.glyphs) == 0:
return nil, fmt.Errorf("font %q: no glyphs", f.name)
}
return f, nil
}
// decodeBitmap turns BDF hex rows into pixels. Each row is padded to whole
// bytes, most significant bit leftmost.
func decodeBitmap(rows []string, w, h int) ([][]bool, error) {
if len(rows) != h {
return nil, fmt.Errorf("bitmap has %d rows, BBX says %d", len(rows), h)
}
stride := (w + 7) / 8
pix := make([][]bool, h)
for i, row := range rows {
raw, err := hex.DecodeString(row)
if err != nil {
return nil, fmt.Errorf("row %d: %w", i, err)
}
if len(raw) < stride {
return nil, fmt.Errorf("row %d: %d bytes, want %d", i, len(raw), stride)
}
line := make([]bool, w)
for j := range line {
line[j] = raw[j/8]>>(7-j%8)&1 == 1
}
pix[i] = line
}
return pix, nil
}
// runs lays out s on one line, returning its ink. A rune the font has no
// glyph for is an error: a blank in a card title would ship unnoticed.
func (f *bdfFont) runs(s string) ([]textRun, error) {
var out []textRun
x := 0
for _, r := range s {
g, ok := f.glyphs[r]
if !ok {
return nil, fmt.Errorf("font %q has no glyph for %q", f.name, r)
}
for i, row := range g.pix {
// Ink box bottom sits yoff above the baseline, which is at
// row index ascent; row i counts down from the box's top.
y := f.ascent - g.yoff - g.h + i
for j := 0; j < len(row); {
if !row[j] {
j++
continue
}
k := j
for k < len(row) && row[k] {
k++
}
out = append(out, textRun{X: x + g.xoff + j, Y: y, W: k - j})
j = k
}
}
x += g.advance
}
return out, nil
}
// width is the advance width of s in font pixels.
func (f *bdfFont) width(s string) (int, error) {
w := 0
for _, r := range s {
g, ok := f.glyphs[r]
if !ok {
return 0, fmt.Errorf("font %q has no glyph for %q", f.name, r)
}
w += g.advance
}
return w, nil
}