summaryrefslogtreecommitdiff
path: root/gui/draw/font.go
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2026-08-22 13:16:35 -0400
committerCalvin Morrison <calvin@pobox.com>2026-08-22 13:16:35 -0400
commit9aacce8b3b54060d0037eca897856d7273e6e5e8 (patch)
treea98ea0cb151a7ded56fe681d6782986b2807dfcf /gui/draw/font.go
parent5ef4699d05bc919255449a9af780f216a0589a72 (diff)
gui: a /dev/draw layer in go, with no cgo and no devdraw
9fans.net/go/draw builds for plan9 but shells out to plan9port's devdraw, which 9front does not have. This talks to /dev/draw itself: the protocol is file i/o, so a pure-go client is a few hundred lines under an already-complete idea. draw/keyboard.go opens /dev/cons and turns the console raw, as libdraw's initkeyboard does. Without it rio keeps its line editor on the window and paints what you type over the drawing. draw/snarf.go writes /dev/snarf, which is what every other program on the system means by copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'gui/draw/font.go')
-rw-r--r--gui/draw/font.go336
1 files changed, 336 insertions, 0 deletions
diff --git a/gui/draw/font.go b/gui/draw/font.go
new file mode 100644
index 0000000..b94f2fd
--- /dev/null
+++ b/gui/draw/font.go
@@ -0,0 +1,336 @@
+package draw
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+)
+
+// Fontchar is one glyph's metrics, unpacked from the 6-byte on-disk form.
+type Fontchar struct {
+ X int32 // left edge in the subfont image
+ Top, Bottom, Left uint8
+ Width uint8 // advance
+}
+
+// Subfont is one image of glyphs plus their metrics -- the unit a .font file
+// is assembled from.
+type Subfont struct {
+ Bits *Image
+ N int
+ Height, Ascent int
+ Info []Fontchar // N+1 entries; the last one bounds the last glyph
+}
+
+type frange struct {
+ min, max rune
+ offset int
+ file string
+ sub *Subfont // loaded on first use
+}
+
+// Font is a .font file: a height, an ascent, and a set of rune ranges each
+// served by a subfont.
+type Font struct {
+ d *Display
+ Name string
+ Height, Ascent int
+ ranges []frange
+}
+
+// OpenFont reads a .font file. Subfonts are loaded lazily, because a unicode
+// font names dozens of them and a calendar touches two.
+func (d *Display) OpenFont(name string) (*Font, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, fmt.Errorf("open font: %w", err)
+ }
+ defer f.Close()
+
+ fnt := &Font{d: d, Name: name}
+ dir := path.Dir(name)
+ sc := bufio.NewScanner(f)
+ first := true
+ for sc.Scan() {
+ fld := strings.Fields(sc.Text())
+ if len(fld) == 0 {
+ continue
+ }
+ if first {
+ if len(fld) < 2 {
+ return nil, fmt.Errorf("font %s: bad first line", name)
+ }
+ h, err1 := strconv.ParseInt(fld[0], 0, 32)
+ a, err2 := strconv.ParseInt(fld[1], 0, 32)
+ if err1 != nil || err2 != nil || h <= 0 || a <= 0 {
+ return nil, fmt.Errorf("font %s: bad height/ascent", name)
+ }
+ fnt.Height, fnt.Ascent = int(h), int(a)
+ first = false
+ continue
+ }
+ // min max [offset] file
+ if len(fld) < 3 {
+ continue
+ }
+ min, err1 := strconv.ParseInt(fld[0], 0, 32)
+ max, err2 := strconv.ParseInt(fld[1], 0, 32)
+ if err1 != nil || err2 != nil || min > max {
+ return nil, fmt.Errorf("font %s: bad range %q", name, sc.Text())
+ }
+ r := frange{min: rune(min), max: rune(max)}
+ rest := fld[2:]
+ // The offset is optional and sits before the filename, so it is
+ // only an offset if it parses as a number and something follows.
+ if len(rest) >= 2 {
+ if off, err := strconv.ParseInt(rest[0], 0, 32); err == nil {
+ r.offset = int(off)
+ rest = rest[1:]
+ }
+ }
+ r.file = rest[0]
+ if !path.IsAbs(r.file) {
+ r.file = path.Join(dir, r.file)
+ }
+ fnt.ranges = append(fnt.ranges, r)
+ }
+ if err := sc.Err(); err != nil {
+ return nil, err
+ }
+ if first {
+ return nil, fmt.Errorf("font %s: empty", name)
+ }
+ return fnt, nil
+}
+
+// lookup finds the subfont holding r and r's index within it.
+func (f *Font) lookup(r rune) (*Subfont, int) {
+ for i := range f.ranges {
+ g := &f.ranges[i]
+ if r < g.min || r > g.max {
+ continue
+ }
+ if g.sub == nil {
+ s, err := f.d.readSubfont(g.file)
+ if err != nil {
+ return nil, 0
+ }
+ g.sub = s
+ }
+ n := int(r-g.min) + g.offset
+ if n < 0 || n >= g.sub.N {
+ return nil, 0
+ }
+ return g.sub, n
+ }
+ return nil, 0
+}
+
+// String draws s at p (p is the top-left, as in libdraw) in colour src, and
+// returns the point just past the last glyph.
+func (f *Font) String(dst *Image, p Point, src *Image, s string) Point {
+ for _, r := range s {
+ sub, n := f.lookup(r)
+ if sub == nil {
+ continue
+ }
+ i, i1 := sub.Info[n], sub.Info[n+1]
+ w := i1.X - i.X
+ if w > 0 {
+ Draw(dst,
+ Rect(p.X+int32(i.Left), p.Y+int32(i.Top),
+ p.X+int32(i.Left)+w, p.Y+int32(i.Bottom)),
+ src, sub.Bits, Point{i.X, int32(i.Top)})
+ }
+ p.X += int32(i.Width)
+ }
+ return p
+}
+
+// Width is the advance of s, without drawing it.
+func (f *Font) Width(s string) int32 {
+ var w int32
+ for _, r := range s {
+ sub, n := f.lookup(r)
+ if sub == nil {
+ continue
+ }
+ w += int32(sub.Info[n].Width)
+ }
+ return w
+}
+
+// readSubfont loads one subfont file: a Plan 9 image, then the glyph table.
+//
+// The image is never decompressed here. Plan 9's compressed image format is
+// exactly what the draw device's 'Y' message accepts, so the blocks go
+// straight to the kernel -- which is all libdraw's creadimage does too.
+func (d *Display) readSubfont(name string) (*Subfont, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, fmt.Errorf("open subfont: %w", err)
+ }
+ defer f.Close()
+ r := bufio.NewReader(f)
+
+ var hdr [5 * 12]byte
+ if _, err := io.ReadFull(r, hdr[:11]); err != nil {
+ return nil, fmt.Errorf("%s: short header: %w", name, err)
+ }
+ compressed := string(hdr[:11]) == "compressed\n"
+ if compressed {
+ if _, err := io.ReadFull(r, hdr[:]); err != nil {
+ return nil, fmt.Errorf("%s: short header: %w", name, err)
+ }
+ } else if _, err := io.ReadFull(r, hdr[11:]); err != nil {
+ return nil, fmt.Errorf("%s: short header: %w", name, err)
+ }
+
+ fld := func(i int) int32 {
+ v, _ := strconv.Atoi(trim(string(hdr[i*12 : i*12+12])))
+ return int32(v)
+ }
+ chn := strToChan(trim(string(hdr[0:12])))
+ ir := Rect(fld(1), fld(2), fld(3), fld(4))
+ img, err := d.Alloc(ir, chn, false, 0)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", name, err)
+ }
+
+ if compressed {
+ err = img.loadCompressed(r, ir)
+ } else {
+ err = img.loadRaw(r, ir, chn)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", name, err)
+ }
+
+ // n, height, ascent, then 6 bytes per glyph for n+1 glyphs.
+ var sh [3 * 12]byte
+ if _, err := io.ReadFull(r, sh[:]); err != nil {
+ return nil, fmt.Errorf("%s: short subfont header: %w", name, err)
+ }
+ n, _ := strconv.Atoi(trim(string(sh[0:12])))
+ if n <= 0 || n > 0x7fff {
+ return nil, fmt.Errorf("%s: bad glyph count %d", name, n)
+ }
+ sf := &Subfont{
+ Bits: img,
+ N: n,
+ Height: mustAtoi(trim(string(sh[12:24]))),
+ Ascent: mustAtoi(trim(string(sh[24:36]))),
+ Info: make([]Fontchar, n+1),
+ }
+ pk := make([]byte, 6*(n+1))
+ if _, err := io.ReadFull(r, pk); err != nil {
+ return nil, fmt.Errorf("%s: short glyph table: %w", name, err)
+ }
+ for i := 0; i <= n; i++ {
+ b := pk[i*6:]
+ sf.Info[i] = Fontchar{
+ X: int32(b[0]) | int32(b[1])<<8,
+ Top: b[2],
+ Bottom: b[3],
+ Left: b[4],
+ Width: b[5],
+ }
+ }
+ return sf, nil
+}
+
+func mustAtoi(s string) int { v, _ := strconv.Atoi(s); return v }
+
+// loadCompressed forwards each compressed block to the draw device verbatim.
+func (i *Image) loadCompressed(r io.Reader, ir Rectangle) error {
+ var bh [2 * 12]byte
+ miny := ir.Min.Y
+ for miny != ir.Max.Y {
+ if _, err := io.ReadFull(r, bh[:]); err != nil {
+ return fmt.Errorf("short block header: %w", err)
+ }
+ maxy := int32(mustAtoi(trim(string(bh[0:12]))))
+ nb := mustAtoi(trim(string(bh[12:24])))
+ if maxy <= miny || maxy > ir.Max.Y || nb <= 0 {
+ return fmt.Errorf("bad block: maxy=%d nb=%d", maxy, nb)
+ }
+ buf := make([]byte, 21+nb)
+ buf[0] = 'Y'
+ put32(buf, 1, i.id)
+ put32(buf, 5, uint32(ir.Min.X))
+ put32(buf, 9, uint32(miny))
+ put32(buf, 13, uint32(ir.Max.X))
+ put32(buf, 17, uint32(maxy))
+ if _, err := io.ReadFull(r, buf[21:]); err != nil {
+ return fmt.Errorf("short block: %w", err)
+ }
+ if err := i.d.write(buf); err != nil {
+ return err
+ }
+ miny = maxy
+ }
+ return nil
+}
+
+// loadRaw uploads an uncompressed image a stripe at a time with 'y'.
+func (i *Image) loadRaw(r io.Reader, ir Rectangle, chn uint32) error {
+ depth := chanDepth(chn)
+ if depth == 0 {
+ return fmt.Errorf("bad channel %#x", chn)
+ }
+ bpl := bytesPerLine(ir, depth)
+ // Keep each write comfortably inside the device's iounit.
+ rows := 8000 / bpl
+ if rows < 1 {
+ rows = 1
+ }
+ for y := ir.Min.Y; y < ir.Max.Y; {
+ y1 := y + int32(rows)
+ if y1 > ir.Max.Y {
+ y1 = ir.Max.Y
+ }
+ n := bpl * int(y1-y)
+ buf := make([]byte, 21+n)
+ buf[0] = 'y'
+ put32(buf, 1, i.id)
+ put32(buf, 5, uint32(ir.Min.X))
+ put32(buf, 9, uint32(y))
+ put32(buf, 13, uint32(ir.Max.X))
+ put32(buf, 17, uint32(y1))
+ if _, err := io.ReadFull(r, buf[21:]); err != nil {
+ return fmt.Errorf("short pixel data: %w", err)
+ }
+ if err := i.d.write(buf); err != nil {
+ return err
+ }
+ y = y1
+ }
+ return nil
+}
+
+// chanDepth is bits per pixel for a channel descriptor.
+func chanDepth(c uint32) int {
+ d := 0
+ for ; c != 0; c >>= 8 {
+ d += int(c & 15)
+ }
+ return d
+}
+
+// bytesPerLine mirrors libdraw's unitsperline(r, d, 8), including the
+// negative-min.x case.
+func bytesPerLine(r Rectangle, d int) int {
+ if d <= 0 || d > 32 {
+ return 0
+ }
+ if r.Min.X >= 0 {
+ l := (int(r.Max.X)*d + 7) / 8
+ return l - (int(r.Min.X)*d)/8
+ }
+ l := (int(r.Max.X)*d + 7) / 8
+ return l + (int(-r.Min.X)*d+7)/8
+}