a73x

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
}