git.lucas.co / go_mono
git clone https://git.lucas.co/go_mono.git

commiteecb4e626fdb8a3398bf2a2edf370c6de479d06d
parent8ca9b58be9
authorNigel Tao <[email protected]>
date2015-08-10 18:34
shiny/font: new package for drawing text on an image.

Package font defines an interface for font faces.

Other packages provide font face implementations. For example, a
truetype package (not part of this CL) would provide one based on .ttf
font files.

This CL also introduces the golang.org/x/exp/shiny/font/plan9font
package, a concrete implementation of the font.Face interface for the
Plan 9 bitmap font format.

Change-Id: Iead8914caaa58c7562b18a86b45002ae47486903
Reviewed-on: https://go-review.googlesource.com/13463
Reviewed-by: Rob Pike <[email protected]>

 example/font/main.go        |  84 ++++++++++
 font/font.go                | 114 +++++++++++++
 font/plan9font/plan9font.go | 388 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 586 insertions(+)

diff --git a/example/font/main.go b/example/font/main.go
new file mode 100644
index 0000000..34b16f6
--- /dev/null
+++ b/example/font/main.go
@@ -0,0 +1,84 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build ignore
+//
+// This build tag means that "go install golang.org/x/exp/shiny/..." doesn't
+// install this example program. Use "go run main.go" to run it.
+
+// Font is a basic example of using fonts.
+package main
+
+import (
+	"flag"
+	"image"
+	"image/color"
+	"image/draw"
+	"image/png"
+	"io/ioutil"
+	"log"
+	"os"
+
+	"golang.org/x/exp/shiny/font"
+	"golang.org/x/exp/shiny/font/plan9font"
+	"golang.org/x/image/math/fixed"
+)
+
+var (
+	subfont   = flag.String("subfont", "", `filename of the Plan 9 subfont file, such as "lucsans/lsr.14"`)
+	firstRune = flag.Int("firstrune", 0, "the Unicode code point of the first rune in the subfont file")
+)
+
+func pt(p fixed.Point26_6) image.Point {
+	return image.Point{
+		X: int(p.X+32) >> 6,
+		Y: int(p.Y+32) >> 6,
+	}
+}
+
+func main() {
+	flag.Parse()
+
+	// TODO: mmap the file.
+	if *subfont == "" {
+		flag.Usage()
+		log.Fatal("no subfont specified")
+	}
+	fontData, err := ioutil.ReadFile(*subfont)
+	if err != nil {
+		log.Fatal(err)
+	}
+	face, err := plan9font.ParseSubfont(fontData, rune(*firstRune))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	dst := image.NewRGBA(image.Rect(0, 0, 800, 100))
+	draw.Draw(dst, dst.Bounds(), image.Black, image.Point{}, draw.Src)
+
+	d := &font.Drawer{
+		Dst:  dst,
+		Src:  image.White,
+		Face: face,
+		Dot: fixed.Point26_6{
+			X: 20 << 6,
+			Y: 80 << 6,
+		},
+	}
+	dot0 := pt(d.Dot)
+	d.DrawString("The quick brown fox jumps over the lazy dog.")
+	dot1 := pt(d.Dot)
+
+	dst.SetRGBA(dot0.X, dot0.Y, color.RGBA{0xff, 0x00, 0x00, 0xff})
+	dst.SetRGBA(dot1.X, dot1.Y, color.RGBA{0x00, 0x00, 0xff, 0xff})
+
+	out, err := os.Create("out.png")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer out.Close()
+	if err := png.Encode(out, dst); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/font/font.go b/font/font.go
new file mode 100644
index 0000000..7698772
--- /dev/null
+++ b/font/font.go
@@ -0,0 +1,114 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package font defines an interface for font faces, for drawing text on an
+// image.
+//
+// Other packages provide font face implementations. For example, a truetype
+// package would provide one based on .ttf font files.
+package font
+
+// TODO: move this from golang.org/x/exp to golang.org/x/image ??
+
+import (
+	"image"
+	"image/draw"
+	"io"
+
+	"golang.org/x/image/math/fixed"
+)
+
+// TODO: who is responsible for caches (glyph images, glyph indices, kerns)?
+// The Drawer or the Face?
+
+// Face is a font face. Its glyphs are often derived from a font file, such as
+// "Comic_Sans_MS.ttf", but a face has a specific size, style, weight and
+// hinting. For example, the 12pt and 18pt versions of Comic Sans are two
+// different faces, even if derived from the same font file.
+//
+// A Face is not safe for concurrent use by multiple goroutines, as its methods
+// may re-use implementation-specific caches and mask image buffers.
+//
+// To create a Face, look to other packages that implement specific font file
+// formats.
+type Face interface {
+	io.Closer
+
+	// Glyph returns the draw.DrawMask parameters (dr, mask, maskp) to draw r's
+	// glyph at the sub-pixel destination location dot. It also returns the new
+	// dot after adding the glyph's advance width. It returns !ok if the face
+	// does not contain a glyph for r.
+	//
+	// The contents of the mask image returned by one Glyph call may change
+	// after the next Glyph call. Callers that want to cache the mask must make
+	// a copy.
+	Glyph(dot fixed.Point26_6, r rune) (
+		newDot fixed.Point26_6, dr image.Rectangle, mask image.Image, maskp image.Point, ok bool)
+
+	// Kern returns the horizontal adjustment for the kerning pair (r0, r1). A
+	// positive kern means to move the glyphs further apart.
+	Kern(r0, r1 rune) fixed.Int26_6
+
+	// TODO: per-font and per-glyph Metrics.
+	// TODO: ColoredGlyph for various emoji?
+	// TODO: Ligatures? Shaping?
+}
+
+type MultiFace struct {
+	// TODO.
+}
+
+// TODO: Drawer.Layout or Drawer.Measure methods to measure text without
+// drawing?
+
+// Drawer draws text on a destination image.
+//
+// A Drawer is not safe for concurrent use by multiple goroutines, since its
+// Face is not.
+type Drawer struct {
+	// Dst is the destination image.
+	Dst draw.Image
+	// Src is the source image.
+	Src image.Image
+	// Face provides the glyph mask images.
+	Face Face
+	// Dot is the baseline location to draw the next glyph. The majority of the
+	// affected pixels will be above and to the right of the dot, but some may
+	// be below or to the left. For example, drawing a 'j' in an italic face
+	// may affect pixels below and to the left of the dot.
+	Dot fixed.Point26_6
+
+	// TODO: Clip image.Image?
+	// TODO: SrcP image.Point for Src images other than *image.Uniform? How
+	// does it get updated during DrawString?
+}
+
+// TODO: should DrawString return the last rune drawn, so the next DrawString
+// call can kern beforehand? Or should that be the responsibility of the caller
+// if they really want to do that, since they have to explicitly shift d.Dot
+// anyway?
+//
+// In general, we'd have a DrawBytes([]byte) and DrawRuneReader(io.RuneReader)
+// and the last case can't assume that you can rewind the stream.
+//
+// TODO: how does this work with line breaking: drawing text up until a
+// vertical line? Should DrawString return the number of runes drawn?
+
+// DrawString draws s at the dot and advances the dot's location.
+func (d *Drawer) DrawString(s string) {
+	var prevC rune
+	for i, c := range s {
+		if i != 0 {
+			d.Dot.X += d.Face.Kern(prevC, c)
+		}
+		newDot, dr, mask, maskp, ok := d.Face.Glyph(d.Dot, c)
+		if !ok {
+			// TODO: is falling back on the U+FFFD glyph the responsibility of
+			// the Drawer or the Face?
+			continue
+		}
+		draw.DrawMask(d.Dst, dr, d.Src, image.Point{}, mask, maskp, draw.Over)
+		d.Dot, prevC = newDot, c
+	}
+}
diff --git a/font/plan9font/plan9font.go b/font/plan9font/plan9font.go
new file mode 100644
index 0000000..3193ec7
--- /dev/null
+++ b/font/plan9font/plan9font.go
@@ -0,0 +1,388 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package plan9font implements font faces for the Plan 9 font file format.
+package plan9font
+
+// TODO: have a face use an *image.Alpha instead of plan9Image implementing the
+// image.Image interface? The image/draw code has a fast path for *image.Alpha
+// masks.
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"image"
+	"image/color"
+	"io"
+	"strings"
+
+	"golang.org/x/exp/shiny/font"
+	"golang.org/x/image/math/fixed"
+)
+
+// fontchar describes one character glyph in a subfont.
+//
+// For more detail, look for "struct Fontchar" in
+// http://plan9.bell-labs.com/magic/man2html/2/cachechars
+type fontchar struct {
+	x      uint32 // X position in the image holding the glyphs.
+	top    uint8  // First non-zero scan line.
+	bottom uint8  // Last non-zero scan line.
+	left   int8   // Offset of baseline.
+	width  uint8  // Width of baseline.
+}
+
+func parseFontchars(p []byte) []fontchar {
+	fc := make([]fontchar, len(p)/6)
+	for i := range fc {
+		fc[i] = fontchar{
+			x:      uint32(p[0]) | uint32(p[1])<<8,
+			top:    uint8(p[2]),
+			bottom: uint8(p[3]),
+			left:   int8(p[4]),
+			width:  uint8(p[5]),
+		}
+		p = p[6:]
+	}
+	return fc
+}
+
+// face implements font.Face.
+type face struct {
+	firstRune rune        // First rune in the subfont.
+	n         int         // Number of characters in the subfont.
+	height    int         // Inter-line spacing.
+	ascent    int         // Height above the baseline.
+	fontchars []fontchar  // Character descriptions.
+	img       *plan9Image // Image holding the glyphs.
+}
+
+func (f *face) Close() error                   { return nil }
+func (f *face) Kern(r0, r1 rune) fixed.Int26_6 { return 0 }
+
+func (f *face) Glyph(dot fixed.Point26_6, r rune) (
+	newDot fixed.Point26_6, dr image.Rectangle, mask image.Image, maskp image.Point, ok bool) {
+
+	r -= f.firstRune
+	if r < 0 || f.n <= int(r) {
+		return fixed.Point26_6{}, image.Rectangle{}, nil, image.Point{}, false
+	}
+	i := &f.fontchars[r+0]
+	j := &f.fontchars[r+1]
+
+	newDot = fixed.Point26_6{
+		X: dot.X + fixed.Int26_6(i.width)<<6,
+		Y: dot.Y,
+	}
+	minX := int(dot.X+32)>>6 + int(i.left)
+	minY := int(dot.Y+32)>>6 + int(i.top) - f.ascent
+	dr = image.Rectangle{
+		Min: image.Point{
+			X: minX,
+			Y: minY,
+		},
+		Max: image.Point{
+			X: minX + int(j.x-i.x),
+			Y: minY + int(i.bottom) - int(i.top),
+		},
+	}
+	return newDot, dr, f.img, image.Point{int(i.x), int(i.top)}, true
+}
+
+// ParseFont parses a Plan 9 font file.
+func ParseFont(data []byte, openFunc func(name string) (io.ReadCloser, error)) (*font.MultiFace, error) {
+	panic("TODO")
+}
+
+// ParseSubfont parses a Plan 9 subfont file.
+//
+// firstRune is the first rune in the subfont file. For example, the
+// Phonetic.6.0 subfont, containing glyphs in the range U+0250 to U+02E9, would
+// set firstRune to '\u0250'.
+func ParseSubfont(data []byte, firstRune rune) (font.Face, error) {
+	data, m, err := parseImage(data)
+	if err != nil {
+		return nil, err
+	}
+	if len(data) < 3*12 {
+		return nil, errors.New("plan9font: invalid subfont: header too short")
+	}
+	n := atoi(data[0*12:])
+	height := atoi(data[1*12:])
+	ascent := atoi(data[2*12:])
+	data = data[3*12:]
+	if len(data) != 6*(n+1) {
+		return nil, errors.New("plan9font: invalid subfont: data length mismatch")
+	}
+	return &face{
+		firstRune: firstRune,
+		n:         n,
+		height:    height,
+		ascent:    ascent,
+		fontchars: parseFontchars(data),
+		img:       m,
+	}, nil
+}
+
+// plan9Image implements that subset of the Plan 9 image feature set that is
+// used by this font file format.
+//
+// Some features, such as the repl bit and a clip rectangle, are omitted for
+// simplicity.
+type plan9Image struct {
+	depth int             // Depth of the pixels in bits.
+	width int             // Width in bytes of a single scan line.
+	rect  image.Rectangle // Extent of the image.
+	pix   []byte          // Pixel bits.
+}
+
+func (m *plan9Image) byteoffset(x, y int) int {
+	a := y * m.width
+	if m.depth < 8 {
+		// We need to always round down, but Go rounds toward zero.
+		np := 8 / m.depth
+		if x < 0 {
+			return a + (x-np+1)/np
+		}
+		return a + x/np
+	}
+	return a + x*(m.depth/8)
+}
+
+func (m *plan9Image) Bounds() image.Rectangle { return m.rect }
+func (m *plan9Image) ColorModel() color.Model { return color.AlphaModel }
+
+func (m *plan9Image) At(x, y int) color.Color {
+	if (image.Point{x, y}).In(m.rect) {
+		b := m.pix[m.byteoffset(x, y)]
+		switch m.depth {
+		case 1:
+			// CGrey, 1.
+			mask := uint8(1 << uint8(7-x&7))
+			if (b & mask) != 0 {
+				return color.Alpha{0xff}
+			}
+			return color.Alpha{0x00}
+		case 2:
+			// CGrey, 2.
+			shift := uint(x&3) << 1
+			// Place pixel at top of word.
+			y := b << shift
+			y &= 0xc0
+			// Replicate throughout.
+			y |= y >> 2
+			y |= y >> 4
+			return color.Alpha{y}
+		}
+	}
+	return color.Alpha{0x00}
+}
+
+var compressed = []byte("compressed\n")
+
+func parseImage(data []byte) (remainingData []byte, m *plan9Image, retErr error) {
+	if !bytes.HasPrefix(data, compressed) {
+		return nil, nil, errors.New("plan9font: unsupported uncompressed format")
+	}
+	data = data[len(compressed):]
+
+	const hdrSize = 5 * 12
+	if len(data) < hdrSize {
+		return nil, nil, errors.New("plan9font: invalid image: header too short")
+	}
+	hdr, data := data[:hdrSize], data[hdrSize:]
+
+	// Distinguish new channel descriptor from old ldepth. Channel descriptors
+	// have letters as well as numbers, while ldepths are a single digit
+	// formatted as %-11d.
+	new := false
+	for m := 0; m < 10; m++ {
+		if hdr[m] != ' ' {
+			new = true
+			break
+		}
+	}
+	if hdr[11] != ' ' {
+		return nil, nil, errors.New("plan9font: invalid image: bad header")
+	}
+	if !new {
+		return nil, nil, errors.New("plan9font: unsupported ldepth format")
+	}
+
+	depth := 0
+	switch s := strings.TrimSpace(string(hdr[:1*12])); s {
+	default:
+		return nil, nil, fmt.Errorf("plan9font: unsupported pixel format %q", s)
+	case "k1":
+		depth = 1
+	case "k2":
+		depth = 2
+	}
+	r := ator(hdr[1*12:])
+	if r.Min.X > r.Max.X || r.Min.Y > r.Max.Y {
+		return nil, nil, errors.New("plan9font: invalid image: bad rectangle")
+	}
+
+	width := bytesPerLine(r, depth)
+	m = &plan9Image{
+		depth: depth,
+		width: width,
+		rect:  r,
+		pix:   make([]byte, width*r.Dy()),
+	}
+
+	miny := r.Min.Y
+	for miny != r.Max.Y {
+		if len(data) < 2*12 {
+			return nil, nil, errors.New("plan9font: invalid image: data band too short")
+		}
+		maxy := atoi(data[0*12:])
+		nb := atoi(data[1*12:])
+		data = data[2*12:]
+
+		if len(data) < nb {
+			return nil, nil, errors.New("plan9font: invalid image: data band length mismatch")
+		}
+		buf := data[:nb]
+		data = data[nb:]
+
+		if maxy <= miny || r.Max.Y < maxy {
+			return nil, nil, fmt.Errorf("plan9font: bad maxy %d", maxy)
+		}
+		// An old-format image would flip the bits here, but we don't support
+		// the old format.
+		rr := r
+		rr.Min.Y = miny
+		rr.Max.Y = maxy
+		if err := decompress(m, rr, buf); err != nil {
+			return nil, nil, err
+		}
+		miny = maxy
+	}
+	return data, m, nil
+}
+
+// Compressed data are sequences of byte codes. If the first byte b has the
+// 0x80 bit set, the next (b^0x80)+1 bytes are data. Otherwise, these two bytes
+// specify a previous string to repeat.
+const (
+	compShortestMatch = 3    // shortest match possible.
+	compWindowSize    = 1024 // window size.
+)
+
+var (
+	errDecompressBufferTooSmall = errors.New("plan9font: decompress: buffer too small")
+	errDecompressPhaseError     = errors.New("plan9font: decompress: phase error")
+)
+
+func decompress(m *plan9Image, r image.Rectangle, data []byte) error {
+	if !r.In(m.rect) {
+		return errors.New("plan9font: decompress: bad rectangle")
+	}
+	bpl := bytesPerLine(r, m.depth)
+	mem := make([]byte, compWindowSize)
+	memi := 0
+	omemi := -1
+	y := r.Min.Y
+	linei := m.byteoffset(r.Min.X, y)
+	eline := linei + bpl
+	datai := 0
+	for {
+		if linei == eline {
+			y++
+			if y == r.Max.Y {
+				break
+			}
+			linei = m.byteoffset(r.Min.X, y)
+			eline = linei + bpl
+		}
+		if datai == len(data) {
+			return errDecompressBufferTooSmall
+		}
+		c := data[datai]
+		datai++
+		if c >= 128 {
+			for cnt := c - 128 + 1; cnt != 0; cnt-- {
+				if datai == len(data) {
+					return errDecompressBufferTooSmall
+				}
+				if linei == eline {
+					return errDecompressPhaseError
+				}
+				m.pix[linei] = data[datai]
+				linei++
+				mem[memi] = data[datai]
+				memi++
+				datai++
+				if memi == len(mem) {
+					memi = 0
+				}
+			}
+		} else {
+			if datai == len(data) {
+				return errDecompressBufferTooSmall
+			}
+			offs := int(data[datai]) + ((int(c) & 3) << 8) + 1
+			datai++
+			if memi < offs {
+				omemi = memi + (compWindowSize - offs)
+			} else {
+				omemi = memi - offs
+			}
+			for cnt := (c >> 2) + compShortestMatch; cnt != 0; cnt-- {
+				if linei == eline {
+					return errDecompressPhaseError
+				}
+				m.pix[linei] = mem[omemi]
+				linei++
+				mem[memi] = mem[omemi]
+				memi++
+				omemi++
+				if omemi == len(mem) {
+					omemi = 0
+				}
+				if memi == len(mem) {
+					memi = 0
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func ator(b []byte) image.Rectangle {
+	return image.Rectangle{atop(b), atop(b[2*12:])}
+}
+
+func atop(b []byte) image.Point {
+	return image.Pt(atoi(b), atoi(b[12:]))
+}
+
+func atoi(b []byte) int {
+	i := 0
+	for ; i < len(b) && b[i] == ' '; i++ {
+	}
+	n := 0
+	for ; i < len(b) && '0' <= b[i] && b[i] <= '9'; i++ {
+		n = n*10 + int(b[i]) - '0'
+	}
+	return n
+}
+
+func bytesPerLine(r image.Rectangle, depth int) int {
+	if depth <= 0 || 32 < depth {
+		panic("invalid depth")
+	}
+	var l int
+	if r.Min.X >= 0 {
+		l = (r.Max.X*depth + 7) / 8
+		l -= (r.Min.X * depth) / 8
+	} else {
+		// Make positive before divide.
+		t := (-r.Min.X*depth + 7) / 8
+		l = t + (r.Max.X*depth+7)/8
+	}
+	return l
+}