shiny/font/plan9font: implement ParseFont.
Also delete font.MultiFace. We can resurrect a font.MultiFace type if we
need it in the future, but for now, it's simpler if it lives in the
plan9font package.
Change-Id: I1493b47696c323424e7d91cb7fac15505bfdd023
Reviewed-on: https://go-review.googlesource.com/13520
Reviewed-by: Rob Pike <r@golang.org>
diff --git a/example/font/main.go b/example/font/main.go
index 34b16f6..10909f4 100644
--- a/example/font/main.go
+++ b/example/font/main.go
@@ -19,6 +19,8 @@
"io/ioutil"
"log"
"os"
+ "path/filepath"
+ "strings"
"golang.org/x/exp/shiny/font"
"golang.org/x/exp/shiny/font/plan9font"
@@ -26,8 +28,9 @@
)
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")
+ fontFlag = flag.String("font", "",
+ `filename of the Plan 9 font or subfont file, such as "lucsans/unicode.8.font" or "lucsans/lsr.14"`)
+ firstRuneFlag = flag.Int("firstrune", 0, "the Unicode code point of the first rune in the subfont file")
)
func pt(p fixed.Point26_6) image.Point {
@@ -40,38 +43,56 @@
func main() {
flag.Parse()
- // TODO: mmap the file.
- if *subfont == "" {
+ // TODO: mmap the files.
+ if *fontFlag == "" {
flag.Usage()
- log.Fatal("no subfont specified")
+ log.Fatal("no font 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)
+ var face font.Face
+ if strings.HasSuffix(*fontFlag, ".font") {
+ fontData, err := ioutil.ReadFile(*fontFlag)
+ if err != nil {
+ log.Fatal(err)
+ }
+ dir := filepath.Dir(*fontFlag)
+ face, err = plan9font.ParseFont(fontData, func(name string) ([]byte, error) {
+ return ioutil.ReadFile(filepath.Join(dir, filepath.FromSlash(name)))
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ } else {
+ fontData, err := ioutil.ReadFile(*fontFlag)
+ if err != nil {
+ log.Fatal(err)
+ }
+ face, err = plan9font.ParseSubfont(fontData, rune(*firstRuneFlag))
+ if err != nil {
+ log.Fatal(err)
+ }
}
- dst := image.NewRGBA(image.Rect(0, 0, 800, 100))
+ dst := image.NewRGBA(image.Rect(0, 0, 800, 300))
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})
+ ss := []string{
+ "The quick brown fox jumps over the lazy dog.",
+ "Hello, 世界.",
+ "U+FFFD is \ufffd.",
+ }
+ for i, s := range ss {
+ d.Dot = fixed.P(20, 100*i+80)
+ dot0 := pt(d.Dot)
+ d.DrawString(s)
+ 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 {
diff --git a/font/font.go b/font/font.go
index 7698772..b3647f5 100644
--- a/font/font.go
+++ b/font/font.go
@@ -55,10 +55,6 @@
// TODO: Ligatures? Shaping?
}
-type MultiFace struct {
- // TODO.
-}
-
// TODO: Drawer.Layout or Drawer.Measure methods to measure text without
// drawing?
diff --git a/font/plan9font/plan9font.go b/font/plan9font/plan9font.go
index 3193ec7..35d6ee8 100644
--- a/font/plan9font/plan9font.go
+++ b/font/plan9font/plan9font.go
@@ -2,12 +2,14 @@
// 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 implements font faces for the Plan 9 font and subfont file
+// formats. These formats are described at
+// http://plan9.bell-labs.com/magic/man2html/6/font
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.
+// TODO: have a subface 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"
@@ -15,7 +17,8 @@
"fmt"
"image"
"image/color"
- "io"
+ "log"
+ "strconv"
"strings"
"golang.org/x/exp/shiny/font"
@@ -49,8 +52,8 @@
return fc
}
-// face implements font.Face.
-type face struct {
+// subface implements font.Face for a Plan 9 subfont.
+type subface struct {
firstRune rune // First rune in the subfont.
n int // Number of characters in the subfont.
height int // Inter-line spacing.
@@ -59,10 +62,10 @@
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 *subface) Close() error { return nil }
+func (f *subface) Kern(r0, r1 rune) fixed.Int26_6 { return 0 }
-func (f *face) Glyph(dot fixed.Point26_6, r rune) (
+func (f *subface) 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
@@ -91,9 +94,132 @@
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")
+// runeRange maps a single rune range [lo, hi] to a lazily loaded subface. Both
+// ends of the range are inclusive.
+type runeRange struct {
+ lo, hi rune
+ offset rune // subfont index that the lo rune maps to.
+ relFilename string
+ subface *subface
+ bad bool
+}
+
+// face implements font.Face for a Plan 9 font.
+//
+// It maps multiple rune ranges to *subface values. Rune ranges may overlap;
+// the first match wins.
+type face struct {
+ height int
+ ascent int
+ readFile func(relFilename string) ([]byte, error)
+ runeRanges []runeRange
+}
+
+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) {
+
+ // Fall back on U+FFFD if we can't find r.
+ for _, rr := range [2]rune{r, '\ufffd'} {
+ // We have to do linear, not binary search. plan9port's
+ // lucsans/unicode.8.font says:
+ // 0x2591 0x2593 ../luc/Altshades.7.0
+ // 0x2500 0x25ee ../luc/FormBlock.7.0
+ // and the rune ranges overlap.
+ for i := range f.runeRanges {
+ x := &f.runeRanges[i]
+ if rr < x.lo || x.hi < rr || x.bad {
+ continue
+ }
+ if x.subface == nil {
+ data, err := f.readFile(x.relFilename)
+ if err != nil {
+ log.Printf("plan9font: couldn't read subfont %q: %v", x.relFilename, err)
+ x.bad = true
+ continue
+ }
+ sub, err := ParseSubfont(data, x.lo-x.offset)
+ if err != nil {
+ log.Printf("plan9font: couldn't parse subfont %q: %v", x.relFilename, err)
+ x.bad = true
+ continue
+ }
+ x.subface = sub.(*subface)
+ }
+ return x.subface.Glyph(dot, rr)
+ }
+ }
+ return fixed.Point26_6{}, image.Rectangle{}, nil, image.Point{}, false
+}
+
+// ParseFont parses a Plan 9 font file. data is the contents of that font file,
+// which gives relative filenames for subfont files. readFile returns the
+// contents of those subfont files. It is similar to io/ioutil's ReadFile
+// function, except that it takes a relative filename instead of an absolute
+// one.
+func ParseFont(data []byte, readFile func(relFilename string) ([]byte, error)) (font.Face, error) {
+ f := &face{
+ readFile: readFile,
+ }
+ // TODO: don't use strconv, to avoid the conversions from []byte to string?
+ for first := true; len(data) > 0; first = false {
+ i := bytes.IndexByte(data, '\n')
+ if i < 0 {
+ return nil, errors.New("plan9font: invalid font: no final newline")
+ }
+ row := string(data[:i])
+ data = data[i+1:]
+ if first {
+ height, s, ok := nextInt32(row)
+ if !ok {
+ return nil, fmt.Errorf("plan9font: invalid font: invalid header %q", row)
+ }
+ ascent, s, ok := nextInt32(s)
+ if !ok {
+ return nil, fmt.Errorf("plan9font: invalid font: invalid header %q", row)
+ }
+ if height < 0 || 0xffff < height || ascent < 0 || 0xffff < ascent {
+ return nil, fmt.Errorf("plan9font: invalid font: invalid header %q", row)
+ }
+ f.height, f.ascent = int(height), int(ascent)
+ continue
+ }
+ lo, s, ok := nextInt32(row)
+ if !ok {
+ return nil, fmt.Errorf("plan9font: invalid font: invalid row %q", row)
+ }
+ hi, s, ok := nextInt32(s)
+ if !ok {
+ return nil, fmt.Errorf("plan9font: invalid font: invalid row %q", row)
+ }
+ offset, s, _ := nextInt32(s)
+
+ f.runeRanges = append(f.runeRanges, runeRange{
+ lo: lo,
+ hi: hi,
+ offset: offset,
+ relFilename: s,
+ })
+ }
+ return f, nil
+}
+
+func nextInt32(s string) (ret int32, remaining string, ok bool) {
+ i := 0
+ for ; i < len(s) && s[i] <= ' '; i++ {
+ }
+ j := i
+ for ; j < len(s) && s[j] > ' '; j++ {
+ }
+ n, err := strconv.ParseInt(s[i:j], 0, 32)
+ if err != nil {
+ return 0, s, false
+ }
+ for ; j < len(s) && s[j] <= ' '; j++ {
+ }
+ return int32(n), s[j:], true
}
// ParseSubfont parses a Plan 9 subfont file.
@@ -116,7 +242,7 @@
if len(data) != 6*(n+1) {
return nil, errors.New("plan9font: invalid subfont: data length mismatch")
}
- return &face{
+ return &subface{
firstRune: firstRune,
n: n,
height: height,