From eecb4e626fdb8a3398bf2a2edf370c6de479d06d Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Mon, 10 Aug 2015 18:34:31 +1000 Subject: [PATCH 2/7] 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 --- example/font/main.go | 84 ++++++++ font/font.go | 114 +++++++++++ font/plan9font/plan9font.go | 388 ++++++++++++++++++++++++++++++++++++ 3 files changed, 586 insertions(+) create mode 100644 example/font/main.go create mode 100644 font/font.go create mode 100644 font/plan9font/plan9font.go 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 +} From e87ffe258cc601c14401f40382a54665841852cb Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Tue, 11 Aug 2015 17:49:26 +1000 Subject: [PATCH 3/7] 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 --- example/font/main.go | 67 ++++++++++------ font/font.go | 4 - font/plan9font/plan9font.go | 154 ++++++++++++++++++++++++++++++++---- 3 files changed, 184 insertions(+), 41 deletions(-) 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 @@ import ( "io/ioutil" "log" "os" + "path/filepath" + "strings" "golang.org/x/exp/shiny/font" "golang.org/x/exp/shiny/font/plan9font" @@ -26,8 +28,9 @@ import ( ) 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 pt(p fixed.Point26_6) image.Point { 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 @@ type Face interface { // 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 @@ import ( "fmt" "image" "image/color" - "io" + "log" + "strconv" "strings" "golang.org/x/exp/shiny/font" @@ -49,8 +52,8 @@ func parseFontchars(p []byte) []fontchar { 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 @@ type face struct { 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 @@ func (f *face) Glyph(dot fixed.Point26_6, r rune) ( 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 @@ func ParseSubfont(data []byte, firstRune rune) (font.Face, error) { 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, From ab08d42a8a37c5f1d5a4481aca35e70bd05b643a Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Wed, 19 Aug 2015 16:33:39 +1000 Subject: [PATCH 4/7] shiny/font: add Hinting, Stretch, Style and Weight option types. These will be used by other vector font packages, such as the truetype package, to select specific faces. Change-Id: I3db8c97335089c2076811e8f85d9a8868fc900bd Reviewed-on: https://go-review.googlesource.com/13714 Reviewed-by: Rob Pike --- font/font.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/font/font.go b/font/font.go index b3647f5..c370986 100644 --- a/font/font.go +++ b/font/font.go @@ -108,3 +108,59 @@ func (d *Drawer) DrawString(s string) { d.Dot, prevC = newDot, c } } + +// Hinting selects how to quantize a vector font's glyph nodes. +// +// Not all fonts support hinting. +type Hinting int + +const ( + HintingNone Hinting = iota + HintingVertical + HintingFull +) + +// Stretch selects a normal, condensed, or expanded face. +// +// Not all fonts support stretches. +type Stretch int + +const ( + StretchUltraCondensed Stretch = -4 + StretchExtraCondensed Stretch = -3 + StretchCondensed Stretch = -2 + StretchSemiCondensed Stretch = -1 + StretchNormal Stretch = +0 + StretchSemiExpanded Stretch = +1 + StretchExpanded Stretch = +2 + StretchExtraExpanded Stretch = +3 + StretchUltraExpanded Stretch = +4 +) + +// Style selects a normal, italic, or oblique face. +// +// Not all fonts support styles. +type Style int + +const ( + StyleNormal Style = iota + StyleItalic + StyleOblique +) + +// Weight selects a normal, light or bold face. +// +// Not all fonts support weights. +type Weight int + +const ( + WeightThin Weight = 100 + WeightExtraLight Weight = 200 + WeightLight Weight = 300 + WeightNormal Weight = 400 + WeightMedium Weight = 500 + WeightSemiBold Weight = 600 + WeightBold Weight = 700 + WeightExtraBold Weight = 800 + WeightBlack Weight = 900 +) From 627898392a4c6304ded005afc7bc944fe674391a Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Mon, 24 Aug 2015 14:01:27 +1000 Subject: [PATCH 5/7] shiny/font: add per-glyph metrics. Change-Id: Ie5c7e29b4eb7bd87b8e99de941f2f94b042e268f Reviewed-on: https://go-review.googlesource.com/13827 Reviewed-by: Rob Pike --- font/font.go | 43 ++++++++++++++++++++++++++++--- font/plan9font/plan9font.go | 50 +++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/font/font.go b/font/font.go index c370986..8f8f4c1 100644 --- a/font/font.go +++ b/font/font.go @@ -37,8 +37,9 @@ type Face interface { // 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. + // 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 @@ -46,11 +47,26 @@ type Face interface { Glyph(dot fixed.Point26_6, r rune) ( newDot fixed.Point26_6, dr image.Rectangle, mask image.Image, maskp image.Point, ok bool) + // GlyphBounds returns the bounding box of r's glyph, drawn at a dot equal + // to the origin, and that glyph's advance width. + // + // It returns !ok if the face does not contain a glyph for r. + // + // The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y. A + // visual depiction of what these metrics are is at + // https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png + GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) + + // GlyphAdvance returns the advance width of r's glyph. + // + // It returns !ok if the face does not contain a glyph for r. + GlyphAdvance(r rune) (advance fixed.Int26_6, 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: per-font Metrics. // TODO: ColoredGlyph for various emoji? // TODO: Ligatures? Shaping? } @@ -102,6 +118,7 @@ func (d *Drawer) DrawString(s string) { if !ok { // TODO: is falling back on the U+FFFD glyph the responsibility of // the Drawer or the Face? + // TODO: set prevC = '\ufffd'? continue } draw.DrawMask(d.Dst, dr, d.Src, image.Point{}, mask, maskp, draw.Over) @@ -109,6 +126,26 @@ func (d *Drawer) DrawString(s string) { } } +// MeasureString returns how far dot would advance by drawing s. +func (d *Drawer) MeasureString(s string) (advance fixed.Int26_6) { + var prevC rune + for i, c := range s { + if i != 0 { + advance += d.Face.Kern(prevC, c) + } + a, ok := d.Face.GlyphAdvance(c) + if !ok { + // TODO: is falling back on the U+FFFD glyph the responsibility of + // the Drawer or the Face? + // TODO: set prevC = '\ufffd'? + continue + } + advance += a + prevC = c + } + return advance +} + // Hinting selects how to quantize a vector font's glyph nodes. // // Not all fonts support hinting. diff --git a/font/plan9font/plan9font.go b/font/plan9font/plan9font.go index 35d6ee8..abcc655 100644 --- a/font/plan9font/plan9font.go +++ b/font/plan9font/plan9font.go @@ -94,6 +94,31 @@ func (f *subface) Glyph(dot fixed.Point26_6, r rune) ( return newDot, dr, f.img, image.Point{int(i.x), int(i.top)}, true } +func (f *subface) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { + r -= f.firstRune + if r < 0 || f.n <= int(r) { + return fixed.Rectangle26_6{}, 0, false + } + i := &f.fontchars[r+0] + j := &f.fontchars[r+1] + + bounds = fixed.R( + int(i.left), + int(i.top)-f.ascent, + int(i.left)+int(j.x-i.x), + int(i.bottom)-f.ascent, + ) + return bounds, fixed.Int26_6(i.width) << 6, true +} + +func (f *subface) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) { + r -= f.firstRune + if r < 0 || f.n <= int(r) { + return 0, false + } + return fixed.Int26_6(f.fontchars[r].width) << 6, true +} + // runeRange maps a single rune range [lo, hi] to a lazily loaded subface. Both // ends of the range are inclusive. type runeRange struct { @@ -121,6 +146,27 @@ 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) { + if s, rr := f.subface(r); s != nil { + return s.Glyph(dot, rr) + } + return fixed.Point26_6{}, image.Rectangle{}, nil, image.Point{}, false +} + +func (f *face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { + if s, rr := f.subface(r); s != nil { + return s.GlyphBounds(rr) + } + return fixed.Rectangle26_6{}, 0, false +} + +func (f *face) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) { + if s, rr := f.subface(r); s != nil { + return s.GlyphAdvance(rr) + } + return 0, false +} + +func (f *face) subface(r rune) (*subface, rune) { // 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 @@ -148,10 +194,10 @@ func (f *face) Glyph(dot fixed.Point26_6, r rune) ( } x.subface = sub.(*subface) } - return x.subface.Glyph(dot, rr) + return x.subface, rr } } - return fixed.Point26_6{}, image.Rectangle{}, nil, image.Point{}, false + return nil, 0 } // ParseFont parses a Plan 9 font file. data is the contents of that font file, From 5ae59125bffc3d2011a3872bb2ae76f29fa0d072 Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Mon, 31 Aug 2015 11:16:53 +1000 Subject: [PATCH 6/7] shiny/font: have Face.Glyph return an advance width, not a new dot. This is consistent with Face.GlyphBounds and Face.GlyphAdvance. Change-Id: I9da6b4f2fdb8f093fc9567c717e8fbbecc624e30 Reviewed-on: https://go-review.googlesource.com/14090 Reviewed-by: David Symonds --- font/font.go | 11 ++++++----- font/plan9font/plan9font.go | 14 +++++--------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/font/font.go b/font/font.go index 8f8f4c1..a9b9729 100644 --- a/font/font.go +++ b/font/font.go @@ -36,8 +36,8 @@ 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. + // glyph at the sub-pixel destination location dot, and that glyph's + // advance width. // // It returns !ok if the face does not contain a glyph for r. // @@ -45,7 +45,7 @@ type Face interface { // 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) + dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) // GlyphBounds returns the bounding box of r's glyph, drawn at a dot equal // to the origin, and that glyph's advance width. @@ -114,7 +114,7 @@ func (d *Drawer) DrawString(s string) { if i != 0 { d.Dot.X += d.Face.Kern(prevC, c) } - newDot, dr, mask, maskp, ok := d.Face.Glyph(d.Dot, c) + dr, mask, maskp, advance, 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? @@ -122,7 +122,8 @@ func (d *Drawer) DrawString(s string) { continue } draw.DrawMask(d.Dst, dr, d.Src, image.Point{}, mask, maskp, draw.Over) - d.Dot, prevC = newDot, c + d.Dot.X += advance + prevC = c } } diff --git a/font/plan9font/plan9font.go b/font/plan9font/plan9font.go index abcc655..89f0a2d 100644 --- a/font/plan9font/plan9font.go +++ b/font/plan9font/plan9font.go @@ -66,19 +66,15 @@ func (f *subface) Close() error { return nil } func (f *subface) Kern(r0, r1 rune) fixed.Int26_6 { return 0 } 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) { + dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { r -= f.firstRune if r < 0 || f.n <= int(r) { - return fixed.Point26_6{}, image.Rectangle{}, nil, image.Point{}, false + return image.Rectangle{}, nil, image.Point{}, 0, 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{ @@ -91,7 +87,7 @@ func (f *subface) Glyph(dot fixed.Point26_6, r rune) ( Y: minY + int(i.bottom) - int(i.top), }, } - return newDot, dr, f.img, image.Point{int(i.x), int(i.top)}, true + return dr, f.img, image.Point{int(i.x), int(i.top)}, fixed.Int26_6(i.width) << 6, true } func (f *subface) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { @@ -144,12 +140,12 @@ 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) { + dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { if s, rr := f.subface(r); s != nil { return s.Glyph(dot, rr) } - return fixed.Point26_6{}, image.Rectangle{}, nil, image.Point{}, false + return image.Rectangle{}, nil, image.Point{}, 0, false } func (f *face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { From b53870fbcf2dabd111ee3c99562452b9f4f72dc3 Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Wed, 26 Aug 2015 11:28:38 +1000 Subject: [PATCH 7/7] shiny/font/plan9font: add an example test. The testdata/fixed font files come from the Plan 9 Port, and were all marked as public domain. The total size of the new testdata is 112K. Change-Id: I8cf5de4e5abd1aec7e6550d330271f2acdc12402 Reviewed-on: https://go-review.googlesource.com/13888 Reviewed-by: Rob Pike --- font/plan9font/example_test.go | 93 ++++++++++++++++++++++++++ font/testdata/fixed/7x13.0000 | Bin 0 -> 3136 bytes font/testdata/fixed/7x13.0100 | Bin 0 -> 3908 bytes font/testdata/fixed/7x13.0200 | Bin 0 -> 3095 bytes font/testdata/fixed/7x13.0300 | Bin 0 -> 2631 bytes font/testdata/fixed/7x13.0400 | Bin 0 -> 3623 bytes font/testdata/fixed/7x13.0500 | Bin 0 -> 2492 bytes font/testdata/fixed/7x13.0E00 | Bin 0 -> 1235 bytes font/testdata/fixed/7x13.1000 | Bin 0 -> 2354 bytes font/testdata/fixed/7x13.1600 | Bin 0 -> 1825 bytes font/testdata/fixed/7x13.1E00 | Bin 0 -> 3713 bytes font/testdata/fixed/7x13.1F00 | Bin 0 -> 3012 bytes font/testdata/fixed/7x13.2000 | Bin 0 -> 2310 bytes font/testdata/fixed/7x13.2100 | Bin 0 -> 3206 bytes font/testdata/fixed/7x13.2200 | Bin 0 -> 3532 bytes font/testdata/fixed/7x13.2300 | Bin 0 -> 1613 bytes font/testdata/fixed/7x13.2400 | Bin 0 -> 1013 bytes font/testdata/fixed/7x13.2500 | Bin 0 -> 2747 bytes font/testdata/fixed/7x13.2600 | Bin 0 -> 1765 bytes font/testdata/fixed/7x13.2700 | Bin 0 -> 1762 bytes font/testdata/fixed/7x13.2800 | Bin 0 -> 1918 bytes font/testdata/fixed/7x13.2A00 | Bin 0 -> 620 bytes font/testdata/fixed/7x13.3000 | Bin 0 -> 569 bytes font/testdata/fixed/7x13.FB00 | Bin 0 -> 912 bytes font/testdata/fixed/7x13.FE00 | Bin 0 -> 387 bytes font/testdata/fixed/7x13.FF00 | Bin 0 -> 1687 bytes font/testdata/fixed/README | 9 +++ font/testdata/fixed/unicode.7x13.font | 68 +++++++++++++++++++ 28 files changed, 170 insertions(+) create mode 100644 font/plan9font/example_test.go create mode 100644 font/testdata/fixed/7x13.0000 create mode 100644 font/testdata/fixed/7x13.0100 create mode 100644 font/testdata/fixed/7x13.0200 create mode 100644 font/testdata/fixed/7x13.0300 create mode 100644 font/testdata/fixed/7x13.0400 create mode 100644 font/testdata/fixed/7x13.0500 create mode 100644 font/testdata/fixed/7x13.0E00 create mode 100644 font/testdata/fixed/7x13.1000 create mode 100644 font/testdata/fixed/7x13.1600 create mode 100644 font/testdata/fixed/7x13.1E00 create mode 100644 font/testdata/fixed/7x13.1F00 create mode 100644 font/testdata/fixed/7x13.2000 create mode 100644 font/testdata/fixed/7x13.2100 create mode 100644 font/testdata/fixed/7x13.2200 create mode 100644 font/testdata/fixed/7x13.2300 create mode 100644 font/testdata/fixed/7x13.2400 create mode 100644 font/testdata/fixed/7x13.2500 create mode 100644 font/testdata/fixed/7x13.2600 create mode 100644 font/testdata/fixed/7x13.2700 create mode 100644 font/testdata/fixed/7x13.2800 create mode 100644 font/testdata/fixed/7x13.2A00 create mode 100644 font/testdata/fixed/7x13.3000 create mode 100644 font/testdata/fixed/7x13.FB00 create mode 100644 font/testdata/fixed/7x13.FE00 create mode 100644 font/testdata/fixed/7x13.FF00 create mode 100644 font/testdata/fixed/README create mode 100644 font/testdata/fixed/unicode.7x13.font diff --git a/font/plan9font/example_test.go b/font/plan9font/example_test.go new file mode 100644 index 0000000..071fcfa --- /dev/null +++ b/font/plan9font/example_test.go @@ -0,0 +1,93 @@ +// 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_test + +import ( + "image" + "image/draw" + "io/ioutil" + "log" + "os" + "path" + "path/filepath" + + "golang.org/x/exp/shiny/font" + "golang.org/x/exp/shiny/font/plan9font" + "golang.org/x/image/math/fixed" +) + +func ExampleParseFont() { + readFile := func(name string) ([]byte, error) { + return ioutil.ReadFile(filepath.FromSlash(path.Join("../testdata/fixed", name))) + } + fontData, err := readFile("unicode.7x13.font") + if err != nil { + log.Fatal(err) + } + face, err := plan9font.ParseFont(fontData, readFile) + if err != nil { + log.Fatal(err) + } + // TODO: derive the ascent from the face's metrics. + const ascent = 11 + + dst := image.NewRGBA(image.Rect(0, 0, 4*7, 13)) + draw.Draw(dst, dst.Bounds(), image.Black, image.Point{}, draw.Src) + d := &font.Drawer{ + Dst: dst, + Src: image.White, + Face: face, + Dot: fixed.P(0, ascent), + } + // Draw: + // - U+0053 LATIN CAPITAL LETTER S + // - U+03A3 GREEK CAPITAL LETTER SIGMA + // - U+222B INTEGRAL + // - U+3055 HIRAGANA LETTER SA + // The testdata does not contain the CJK subfont files, so U+3055 HIRAGANA + // LETTER SA (さ) should be rendered as U+FFFD REPLACEMENT CHARACTER (�). + // + // The missing subfont file will trigger an "open + // ../testdata/shinonome/k12.3000: no such file or directory" log message. + // This is expected and can be ignored. + d.DrawString("SΣ∫さ") + + // Convert the dst image to ASCII art. + var out []byte + b := dst.Bounds() + for y := b.Min.Y; y < b.Max.Y; y++ { + out = append(out, '0'+byte(y%10), ' ') + for x := b.Min.X; x < b.Max.X; x++ { + if dst.RGBAAt(x, y).R > 0 { + out = append(out, 'X') + } else { + out = append(out, '.') + } + } + // Highlight the last row before the baseline. Glyphs like 'S' without + // descenders should not affect any pixels whose Y coordinate is >= the + // baseline. + if y == ascent-1 { + out = append(out, '_') + } + out = append(out, '\n') + } + os.Stdout.Write(out) + + // Output: + // 0 ..................X......... + // 1 .................X.X........ + // 2 .XXXX..XXXXXX....X.....XXX.. + // 3 X....X.X.........X....XX.XX. + // 4 X.......X........X....X.X.X. + // 5 X........X.......X....XXX.X. + // 6 .XXXX.....X......X....XX.XX. + // 7 .....X...X.......X....XX.XX. + // 8 .....X..X........X....XXXXX. + // 9 X....X.X.........X....XX.XX. + // 0 .XXXX..XXXXXX....X.....XXX.._ + // 1 ...............X.X.......... + // 2 ................X........... +} diff --git a/font/testdata/fixed/7x13.0000 b/font/testdata/fixed/7x13.0000 new file mode 100644 index 0000000000000000000000000000000000000000..9509cdf97f478dc67f631cc67d25490eef825a10 GIT binary patch literal 3136 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 S@91gtL-Z2*84Bsr5Aa{3E_&qv literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.0200 b/font/testdata/fixed/7x13.0200 new file mode 100644 index 0000000000000000000000000000000000000000..e25247ecc749f4c6a9d76644593ead99e2b08db0 GIT binary patch literal 3095 zcmdVc|8o@O9S88w?mc(6mmImh%LWe;!rtWqN2(?PY)^WI&CWqinwle~72C8E`7%Od zHImVWPKvvGdzZVAkh>siZ7m5<5Vc|*ucK314-!OcD`Iu%x6uHlAfq)%Td*>1H}4tk zFZK`U{_xsoo|(@+pXc*D^E|umo^SVT-q72-VSS1G$Czkj+uyk4 zv)YX}EM6+7bKI1Rs)^ zET3E*i1u;Q9?k8K+h|92ImNl=7R^mDy3Q;k5`@sLnohQ)!`+}&dBqZg<3uqmIFe;m zVwZGpX2KLi61X8h6*P^m*L+;t_R&2Kw_lV4oG|JaDX{3>A=#BBAzb9m?Mx8uTl7jw zH{>SkUsv>=`1Gb_m-qa&I~Ci{n6FcspN=(mOv zjuDx>5tdE~E}=f@4T?GD)ELbf2*m_Z*~!P~wb8C3uk%tYA&-tTUJfS|BgNKhO2BRq z6etn!;2s7hJ0}mF;OQ zBzeK>b%$rYA=IRbWRgEb{koDT56@92L8oGi`xrYIDWQiE&k%ZW)+AP6e~&e489)L8|-{&s&RufEW8#OW9kpU;-BcqkCpLU8lPv}>h~-DFJ;5Y-+vN}5 z(PdFTH{c1nS8!t?&#tDAE}gcI>7wL6$`GM#H;^LSaWsOw1N)gQZvMw1%xRb{a# zX+ge!R=4ITyU*Tja^xgpqjaZOFmi?}6owj9Y6+-jmJEpHtw{~js(k5Wv*8iMEqpkUuZzoi%>XMHZ^n06W2`&(R(%Jz zFS&QHBFNhquXS}6LLMuwhYCcg)Ldz!I5sB{X{r^RvDUtZ3dY{JZtvD(aXv5-@$&2Q-e4xcClphNjXDBa6Mr(R z&knyG%eU`qt!RC!H0?a99S)wY=r#g}Cv8uRn-|It9$y~q|LD@Wk?N>v_?+8+w5)Hs z`It7Y_Vr#qf6I%d4^X8py6T5>Qwt5or(72YJiA0%^K59fZP*;2*stWQfkte@BOP9D zPnkDp)|{Jan-n51QyhXuEhc%=f ztcuAr<*qaQ&JpPbEg}?jhd_3BLkDSm&G=27pIioum;A3gqERNIS>&|#l%;hXTUNj- zSOe=|BaFc1a3%aQ?0{c~YvCr?1HS{e!W0~WJK&>m96k;Y!k@ukz)AQjd;|U-PFWKm z`cy0bbLjso{5!k~ZQRW4N}w0|;e5CNUI&-LFGB0Ou%d2;33wa)CO0$g|Akim8<6u| zuor$8>M#RG;6v~+xEDSNpM@{LB0L6PgKxn}p@NIY&z6USCe?rbalc4}Ta5kI= rL-2EOF}x8fum#=%zXChqI`}PkFYJR4z%(3&55gb7-SB{YW)=Pe>1k#m literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.0300 b/font/testdata/fixed/7x13.0300 new file mode 100644 index 0000000000000000000000000000000000000000..86eb33f42fb587cc7a9f356f8a344a775fd56dcb GIT binary patch literal 2631 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 E1dQH-2LJ#7 literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.0E00 b/font/testdata/fixed/7x13.0E00 new file mode 100644 index 0000000000000000000000000000000000000000..7c51a1e511a5919063c2bc729f41ed9815e67342 GIT binary patch literal 1235 zcmZwFe`s4(6bJB=_d@%YzIBPFBh)ybuQ4QD#U%aVHnW=5q-5cWVa>Xd)q$P;qgu7B zf4Ge>^3o<**0C(vGS;zQ)kYGcv{fny4jlAX5L9G>ihoqXR_IVRwt>iCCSM3c==tMw z&$(PK=UndLk3JYVGB`Rq_^w8(#{2eaR$grz<{h@3PxtMu8|Lk89a5M=Z>y=A+?sns@xD0g(dXX$**c|U#5{MNS4 zytQd5>F6v@Y;6h7I~NnB<>WT!!h??GWUI;)UKRUebLwl&>SI5dSgf72QgD{ekKesz z?T>YB`gA()pK?7~pSJqvT_QW%wRlQz@lSOv)^%78-EgQjiQf_%R~psPoYpr{T25bb z9-ZsCd3BZw3zq!__a}`jXVY14%K4HtTj-k%c@uhzFEik&TWac4f`K8~%oHaHB8&5X zsw@2qy7+{C`~Ive?7LOV6)5K}W(wYPV^OEiucREF^=!eLl20b|d0$3$ugInqCD=c? z?=z-wea1?rEqh;z~%6H~P&lI@+LQTdKXOAU>9o0;d{`aHo5>;a})z%V~t%nWJ2%BItw8I_nb=VExgaLRE4!|Ke z0zZZ)U=*H)=intc1#@r)ehqKH1^7Mu8U6-W;TrrKK7x-Sud2+YfjVe_FF-SV8MeXK zU>DpA{qO)h1P9?T9EHcB0#Cvuyr8N)@rIVWx`H#)umHb=-@xzSGW-Gl0{?&|xDM~b jf8Y~X!&Ro<4E69iC_^i>!B=4?^gsrCU=Y5;Ri^k4X&(gZ literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.1000 b/font/testdata/fixed/7x13.1000 new file mode 100644 index 0000000000000000000000000000000000000000..019698c8d105992723c6ad0e43f181a62e703908 GIT binary patch literal 2354 zcmeIyacCP=90%~5NVr=;va5x45n`A^#?_S{^fr{ z-@)g;+&v!ezC7;O3(u!t932}QJua%70Vjz z)b{x&V%=<=(*??GXv z#XRFnS)paq-H7llyi zH>K@lD>?e-N+LVozSLV;b;@1V@8lgPV_1@u$gm8PfPP%=3$5DCD3d;FSuotdCIB+ z%_{||(iI^p7tHE*RG`cTI%m%6mah^%QZQ_xVs&eVDJY3)D31rnYKA|^3&)(pGi3P2 z4Grkxgs(2_?CiRmt7$@O$R>(5eac|#t9w{EWI0k8rVQ(PCFi_-@V3~Tal3xG=XIv4 zcV(?r1^gK!4PstkU^%DSO0XmA?u}$^PLw!+Nt%Okx$fDOIVgb08lWkceyY~LF)IPQ zY^^T?gUyq@zhnPqRMmeS_?QaYW4nRv2L8_tG(kfHfU6&EK|^R8x)+V152KxE4|*6K zM4v!M&}Yyz`U-jiwb5zx4Rju@q6_FE`XTxedIkLgy^4N^E~7u8zoIwM+o*@z_%6}? z;Q{<--4$e9Cy3sM?nduN51@~t2HJ}z(Z|sz(NXj{bPRnJ&7cliMCZ`A&n1Kjhc4>YrMkJbE^cn(>d!CaiJ^2C@`lg*@_FBTy!+jK{3v>|xU{r* zPnU!Bpgc&lyQk*L%uwTG<+5(m4Er7zT`$!-t3g+PVdYtU zT_&NgYmOhJ6Cb%)Suk}bSgku=;58;*_OhDo2PfyfUNPA9%q$bU6Bl@gUdc7h@)Lu9 z;F^0>&(*l#`#BLy+JCLA)lIGQCq?x4gW*)B=LS*Ae`Z#eY_vub7){`Rp1>r_Wbl3> z;+0uRA2;8*k&secS} O2C4HM-~Ir9qW>F8LVdab literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.1E00 b/font/testdata/fixed/7x13.1E00 new file mode 100644 index 0000000000000000000000000000000000000000..3bc5068cfbdcf5a47f9af710fee2b583184d9da6 GIT binary patch literal 3713 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 literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.1F00 b/font/testdata/fixed/7x13.1F00 new file mode 100644 index 0000000000000000000000000000000000000000..43b320be121764eaa703e070c1514324f44573f5 GIT binary patch literal 3012 zcmZwJe{>Yp6$kLQlgaY3nF*6MV#rdMUBr0*53R}(EI*1623sTb*wnU`0tI{8N~+ctYVF&vhtNN| ze|+|yyLaBZcg{QS?2^a7-`vu)VntJzxDqY7tSgEpPItrn>9D%!s3$I zDkCB>y4SEoH2!q_SiE~`QSJVKY}CJ@$#SugepRCEEq6tf zu_;ckWQ!|G{n7NzJH;$*v|I6QadirG?8WcCxoys)R34nW?z8Kko)^SOv5Fw}Js(3Ok5q(($vB)`xul2g`pPCaD7Ur_)Wc#7r zGefL$NHqp0lgfqT)w@I@mMmX9vSB*aBdXIP5)y9;_T|=e+Nqq#4mFg;1VK@JKHt2W zf$F9}k0z=rV=UTZ_XicVTy(H9q8kI%iZ|MqA4*s>z1=B@RB;Lx-mJ=^qM|67yjd+! zAR9ivD!&@Me4dB}wd6#8P$bHJ!h|UW4P||9|N4T}*Rp0gh&PyNuh=xVJnAbydo(k@ zFKBlPgw`G)-f%)uGk3&?&h+M29crQJQMW#LJfhqdw^a=n4VnE)Zk%?yw2t1~w0D`n z8yhZCa>F&li*9ij=o*o9b}3`qQn|gs=?Zla)nZ#6SA9M1fFjcD*bGV)!=gM8p*^-N zMURsnmp?(2r&oS(?%Daz+@n?Y0Nis-{Cp*gfMB7v)tcF8;q4# z`1@(96b_O))-1{4pj{L1VjYSsk|>Hl&#mF z9v$>$^SriCH;wvZyx}6=?6uVXHd8aB&WN25%8Wgge6y{r!)F?uX`+>>ZKh%xBh|#k z(rn#dyn#M9t;|Erv{|G?(%W;Zc6eg+SUZ2W?gf0j#?yY21>_|xq>Rx#)J0`G<8FbG5NoA4pn1RsSf;2LPa4!8+E1)qUKa4$Rv55c4G6#RuHIq-zM^LLQ{ zExZ8#49DR=Q9mKC{~UR%kephESX50nflu_-pt+{1f~u{1{$`LQ1m3G^oJa;PjN_ zj1%&j+mSDT#qb_j1_ST`xCnj+Ho`|>3tSBuY=;|RAKVUi!{^}v_$vGvd;`7-e+A!z ze}o^vtMF6!ADAj8CzaqWFb6uJ8-4}OfnSAw_;pwfzXj{z_uymj2e1uB;Ck4@|7Z}x z<-0!(cfudTpTL*kYw$Q6fp5Wg;qT!k_!syQyoU9;e0@u5QVJ)-Dey~BgR@`}yc_!9 zeQ-Wp2p>#Mc0M6rc?t5%;S;bG{s_iV&*hcf@JYB0?t;T`KYRtg4o||f@ErUNJP)tH u58*%HXHc{z_e!@W`<#$x?8r}rDt_hi{2lO4I2(H4*I*@F0BhmHc=o?@0a_XW literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.2000 b/font/testdata/fixed/7x13.2000 new file mode 100644 index 0000000000000000000000000000000000000000..f9244e1cf9f08933d03d080bd3ddb875f53b8fa6 GIT binary patch literal 2310 zcmdVae{2(F7zgm@uJ6{P>$)43P;e=?GDv{|9h*?yaGey=MbI(kFN{XCGngSv-b4rT z<9)B|R$%NG&WM@P%_WS1Vn`G*kZr+*L=#aGBPJ$bh}IZ2Tf*WHGrfM3X-tSQ{$cd~ z_}rT}_dM_Oy!YNcwe8vZ?X{fOMjgh~RaZ1MvhIOfj_+Jn^2dBp@h!)}z#WF5UF0Td zo7gosL`{Ys%4%(AdUi~0w~q#WO)+v#-=e(j$%(V+H5Kzh&Mavc#N^mjEvs31L{;-V z^&y%rH6yS}_zRp(yCy=;^@Dq8YxBU-^6r&4ei{udj#nu*auyI5Qh!cH8r?kRA0>_K zFEQa;v%#uB*(fq~L+_~|m#!CvTN-Ypu8G{Tp#;rM9HBNd)M{g?ggVfl>kb0^q(-Y7uRyC8ZSNX#ST^;VR>zleo95@j~0+-pM>Jb6%$mF=RzE=Jp4O zcd#;j>?_~;Ifdg5ie2^SMulTl3E%&DG;8qeCj%#DI0YmOV43 zsg-^;`+!PihVX@}75{jHe}x&6cJIjk9o6wx$06g`ccdl1H_jY`$-aRT1rDc&%Y6Z>{$AoWTuQs^w0Pf{ zQZ8ycW2GV_*o)G?dugqi6sC9NVE#ZCyRXCOt8R%_vh6c2ih5g}nH-K14{*6*LMe|C zMaXs}u0{H3+5@vs$NdH~ST2RMRA7afl!}#P<=EF*Egi;t)i>WrG;Ue6NcM)U_|hzw zNz@*=O(?FX<&c9itm9f%9HXR};T;hQWw6z=zISU)_VVrq#X6v+*6|NjDfjBf7>|%y zao#FWyX5RE){J<4~)YWcnH10NTbTSZQ+@xpnq5EjF`Ubg literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.2100 b/font/testdata/fixed/7x13.2100 new file mode 100644 index 0000000000000000000000000000000000000000..c565abb7d261646173a5c30caa7f1a325b4c534c GIT binary patch literal 3206 zcmd7TeQ?wE83*u3ikv83USuKKTpK`k3>eBr0x5*T9N9C>xLxBIr$7fIgqP8!8wuP- z*aafVcH%f`NkZt#SRpn^d1-lD5x3sDl7wy@Y%8OqP22TGNGWu5-G!GPVGCr=cjI=w z>!15$(%nbDlXO2ldZg$1ty=&6sts#4Zd|k4EZ^HI3+|1Q$NVoF7Zg4`>%ZpCUZd zNF|20T)WZDtWuq#JB9WpJ#|wiR=gu&6mL&d^%F?^KqMCRk zqNZ8+Wb9Lt(Y@5=Yv~cnd$}6c-X6_0rI(hpcey6|TBio%f#9(~+-0?gSvl+@ipa^S zs#r743wP*jMvlYKh1Ru{narHtK+S4^jPQ2zV{#g;SF42MDnqBpsZl8X)n~cv3z{-0 zG?(0*Jcno{XVj;cvRM}`0`uF5X5u7zkDEw`L;adLOy(w+%QUy$;g2sQlG?j`T3$5?q8b?WI}`#~k*X%oHv&NoUKMG%^m30L2dDWFluf#F5BF0E*l zc~9J(sY_E9-?+|X56hNC1EQO6vG-adW}Bx`oU*4s@%*~CHB;sT$~lWGG>>kNVQdXxEJ_>Qr4b@R=eMn+E7`NdnW3t!VrELUW&9&>r|?s>f6dbI52 zrPUj@hkoL$C7JDFtnQpK?BpFYZB^HacHqs1eiuDE^niqkoqH>ge6bGPookm67G3V}n*#0txuA!XZz zFrV-5N$pE8W?znp+Vy}n=Xl-7$ekOXR?JdI?xuSunk7fI=$IXAHzox%lPT8Cxg~e7 z)y4~&uk6S+r?Vlc2Re<6+2Z$v@g&(enUrxJ7rEpR?2%qBp8{Ms#`GV zlpYYQoRMDHWK-juX!B38CETK^a#zI5*zJve)o<|#t=(6d3Ja-9vVM5QZyQ^=`{UxW zEsMp%nikd*Rl+Q@uw(4zlI3qnj*Tblo=CkF^TxE4SfDrRmRw}Sh4k)Nu(MQixBG%! zONeLAq={}${Kh9O`AS$39gQp*O|lY7&B)0wZ?$<;$uReEhGjEvuziPFZmMIy%gn~? zvd|KaHG1tf_ueooIsAzZfAQ9`LDeR=FD;mo={DKj3ac%q9)A|yjl`}MVm@-9cQ?jyI%B3n>2BW4WG&PMl zU1oZ|awNiZ`vtCu?pL+6kLr_+CpAZ@VJaKvMrD~$-Kl-`98cuxVJWkjlI5K>>*=Ar zPf2QfY{Aa{o^$cnNU>Lt@uFNUu4(UdYOYJ3U{`=LXj}Iy^J+wKdiMCDn$XRXjFU6? z+uc2;%4F*mt%9MiIP2sLDYIeQIL~N5Wa)NDma;S{5gCTcEdN}TkswDA^Zqlh1cK^9D%9Hf}*rcD8UDz4Nigia5i+p`S3Bg2tEl{ z!gX*x{1My&!>|dq!JV)J?t_P+0bhfs;2C%hz6USB%kVRJlO6qP{}Yor`6uRNQ%3WX z4h48W91kbLX>cZd1iGLXmcS?Aa=02+!VPdURAB>r7VdyQgL~mYcntmmo`k=Fe}L!V zpWy)fD;$Kk;9Y3qM&B+2j)4zAD|Eo=a2EVJ^uR}904{~)@VoE_@Q1J#hF}b~!sp>0 z_#!+6kHcTWUU(Y*5hgotGUmU)kKy0o4fqepm`3m6VK%%Ej)Qji5S#($Km~piJ`T&^ eGPnwU4_3n(SO@FjGq4@*f(dv49yN{rpZ@}k1G%{X literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.2200 b/font/testdata/fixed/7x13.2200 new file mode 100644 index 0000000000000000000000000000000000000000..a992d3521af0e7ef6bdc2571c9464ab4685208d1 GIT binary patch literal 3532 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 JL(-$4=s)CzBGUi> literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.2300 b/font/testdata/fixed/7x13.2300 new file mode 100644 index 0000000000000000000000000000000000000000..8ff099d19e02f6fad93292917f7803c5b6424aad GIT binary patch literal 1613 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 literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.2600 b/font/testdata/fixed/7x13.2600 new file mode 100644 index 0000000000000000000000000000000000000000..1b393c28ba2753ccfd5a68b0b683f35979da9d16 GIT binary patch literal 1765 zcmaLWeN0nV6aetk#{=4~mIt=32(gb+W@((E2+B4rr61brHbF{sI#+aJG-I>VxakD; z`tdBS4k$7weo=Fj_gUG?nvrGf&Qfj5ESauXSGRaPx9 z-~`}=nPQPA;Q<4gMRbG#zF>nuY;y^XTAL&fNL!L%&S91+1scEwpu5cg@q}C$KR7Y| zkURYoAgRUhvEJ$pr@)%+l6kvGV>{RSJ_URC(T0j>6+qER!UsU#yu2~2nGsr*2Qw*f zIRuhYo~jgF6bu>&l5~;_Sq9jQd>SsFBRP-Cj2@GoZgw2%>T_D7cdr~&*NlD6@7~t; z!~Ve@NVir#7Bx#7^|6FV1X4G!W7)@~r|LJxIL|R8jJUNL`$bV{dX=4NG20&IXi07Z zXGlXfX0jP7t%kHTrqPG{)rwejFn#WIQO9(FTA5Nkpe`hOL_GZC_Id#ja29rX5njcuh_h_A7(&8@OF*bN%5h<#z zg3vDRr`1cdE@(OpFmJB6(NwrK@ z!!yu@D^r|mN)@(A%!s{VPPgE^D!w>(^rrnupH_B*(Ky!tr|xoN3PI~IHZT< zryl!wmGs{e<9|K4MOy5mJpcqE9Bp~6GX%oJ) z0|&~0A*F6cxv~U+QGjQ?l!2+HR;dc6LYn?hf2(K!m=y~)K`V%^UM$lpO zKKclKf+ldDR>4P8Q3)zVUqD|(3(!|kEowr`&=u%gXf;}cZbd&rZD<49gziHRqXD!N zJ%gS@!|3>&}e97$=aE(X>Hkp|@kQwF6XAut*OqaiRF0<;VP zUIun{1_mI>2PQ?qqzsr;29sJ~+sPn-W*UM`umF?xVA2gt`hv+2Fc}RdlfYyqm@EL3 HDtpE`qy?4PvZ;9lNApp{|YHii#2jAPCqOB@`3v#O^Na1RE6<6AZxa zZn4-0SKu7(d+u-kGw00Q^UjCs8Q>on76+GvmUu_?C3j@SeHp*Q+rFpkA=oPslP0WQO}xCM9PL5#<I6tw8n~99o6WBjj%boVHfO;18^_~;7AO`i8vkS;9^{f8*n@B z#UpqU&to#CU^+g**Z2uD(a=cOml;}MDJ+LJSPSjY1)E?i?10^|FM6Rb2H_YC!^s$f z^KmJz!Oge}58!b;gGqQD@8AP`j_>d*{zemHU3UsBhNZCrR>QjJhz(JTZLu@UzxHZH;yxE{CR9z2XEFcGidElk75_zFMb4>T~*^;G~1V@WKFm9Zw;qBFW; zOKgwb&>cN-C=SQbI02(D8s}jwuEtHc6ZhjWJdGFe8s5hH_zd6T7yO0BlCHagSQM>L zg;lW*I$#5AhHbDD_Qd|^gZ>zT<1hlJ;w)T<%W)lU#W*~K33v`K<4wGWkMJda!0#xs Ie*HrH1wPyIyZ`_I literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.2A00 b/font/testdata/fixed/7x13.2A00 new file mode 100644 index 0000000000000000000000000000000000000000..71791ace691545674d3417fe10dbcdc5b74d48b2 GIT binary patch literal 620 zcmYe!&n+lQEiO(?;ZlHsY(p3eNE={hn1f{i&qtGkqp?*@fTN{V zZSDFGEXvI-E4QxXFlBIOkZN*b{L@-d@c8(J{S1*33~;;6OyEofWM>!}DljlG@G`Ko ugGoLxDGDZKz@##m)B=-+K&1=}a1yA?0xUvj5@@eI*xq440{!9!_6q=D;Yn2h literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.3000 b/font/testdata/fixed/7x13.3000 new file mode 100644 index 0000000000000000000000000000000000000000..fb830f4fbf7ba957f62b83c140db986580d02159 GIT binary patch literal 569 zcmYe!&n+lQEiO(?;ZlHsY(p3eNE={h8=E4;4UMt0O)L}|7+4ruL>)grU}(`;=&=3( z154wY1C0TJKw*}~)eemdG#OYpI5~tlWI5D13^^F!2AP?_nF=W88!9kR26!3R*%=ss bBp=uus*^yIMZxCFfXyFfB+z%tV1EGsfPW_N literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/7x13.FB00 b/font/testdata/fixed/7x13.FB00 new file mode 100644 index 0000000000000000000000000000000000000000..3a0b30a999c61c749a6c9717f8dcaa15a132f69c GIT binary patch literal 912 zcmd7OO=uHA6bJCxb+D7g)XlOLtHEs6nbs79Y-=GxsZH3*Hc?q5hf)uUwud5OwFe^< z*|v$c5HYD2?X8FSfd}!@o1Xjtdr<1hn;%GTeo#b%6`cN|nu8Z_F6?jqJl+hCck1k! zvGXJ2<0HeGweg*HHj+G(f6ZOVh90NuPftVc+}~{lWP%X+Q&Qr|W%t(R)V4t7TP77! z8XD-nH>weB$|f_OD6(kk94UHfb2dQ*8_efw-hg>Gv2*c|a$@=Pt z>DaEG>gto@<(`R))|Is%-#-0(XK*TR&hFrPUtfmWI@9fR#CAPhctuKQDl8WmOq7rX=M#SIITgW6CMp^+zsU&hiw3Ua$fnsjuWU86zzWBYrRKvO(R%tnoCHEL6O--;Fw!kRd4LhJCTM7BE zl#{10+X2k>Ps+OoarZFH!(;Fy9ED@>0=xuEune!mdANWXr1IrE@IG9G58-3V|t literal 0 HcmV?d00001 diff --git a/font/testdata/fixed/README b/font/testdata/fixed/README new file mode 100644 index 0000000..a39f8a5 --- /dev/null +++ b/font/testdata/fixed/README @@ -0,0 +1,9 @@ +These font files were copied from the Plan 9 Port's font/fixed directory. The +README in that directory states that: "These fonts are converted from the BDFs +in the XFree86 distribution. They were all marked as public domain." + +The Plan 9 Port is at https://github.com/9fans/plan9port and the copy was made +from commit a78b1841 (2015-08-18). + +The unicode.7x13.font file also refers to a ../shinonome directory, but this +testdata does not include those subfont files. diff --git a/font/testdata/fixed/unicode.7x13.font b/font/testdata/fixed/unicode.7x13.font new file mode 100644 index 0000000..337b428 --- /dev/null +++ b/font/testdata/fixed/unicode.7x13.font @@ -0,0 +1,68 @@ +13 10 +0x0000 0x001F 7x13.2400 +0x0000 0x00FF 7x13.0000 +0x0100 0x01FF 7x13.0100 +0x0200 0x02FF 7x13.0200 +0x0300 0x03FF 7x13.0300 +0x0400 0x04FF 7x13.0400 +0x0500 0x05FF 7x13.0500 +0x0E00 0x0EFF 7x13.0E00 +0x1000 0x10FF 7x13.1000 +0x1600 0x16FF 7x13.1600 +0x1E00 0x1EFF 7x13.1E00 +0x1F00 0x1FFF 7x13.1F00 +0x2000 0x20FF 7x13.2000 +0x2100 0x21FF 7x13.2100 +0x2200 0x22FF 7x13.2200 +0x2300 0x23FF 7x13.2300 +0x2400 0x24FF 7x13.2400 +0x2500 0x25FF 7x13.2500 +0x2600 0x26FF 7x13.2600 +0x2700 0x27FF 7x13.2700 +0x2800 0x28FF 7x13.2800 +0x2A00 0x2AFF 7x13.2A00 +0x3000 0x30fe ../shinonome/k12.3000 +0x4e00 0x4ffe ../shinonome/k12.4e00 +0x5005 0x51fe ../shinonome/k12.5005 +0x5200 0x53fa ../shinonome/k12.5200 +0x5401 0x55fe ../shinonome/k12.5401 +0x5606 0x57fc ../shinonome/k12.5606 +0x5800 0x59ff ../shinonome/k12.5800 +0x5a01 0x5bff ../shinonome/k12.5a01 +0x5c01 0x5dfe ../shinonome/k12.5c01 +0x5e02 0x5fff ../shinonome/k12.5e02 +0x600e 0x61ff ../shinonome/k12.600e +0x6200 0x63fa ../shinonome/k12.6200 +0x6406 0x65fb ../shinonome/k12.6406 +0x6602 0x67ff ../shinonome/k12.6602 +0x6802 0x69ff ../shinonome/k12.6802 +0x6a02 0x6bf3 ../shinonome/k12.6a02 +0x6c08 0x6dfb ../shinonome/k12.6c08 +0x6e05 0x6ffe ../shinonome/k12.6e05 +0x7001 0x71ff ../shinonome/k12.7001 +0x7206 0x73fe ../shinonome/k12.7206 +0x7403 0x75ff ../shinonome/k12.7403 +0x7601 0x77fc ../shinonome/k12.7601 +0x7802 0x79fb ../shinonome/k12.7802 +0x7a00 0x7bf7 ../shinonome/k12.7a00 +0x7c00 0x7dfb ../shinonome/k12.7c00 +0x7e01 0x7ffc ../shinonome/k12.7e01 +0x8000 0x81fe ../shinonome/k12.8000 +0x8201 0x83fd ../shinonome/k12.8201 +0x8403 0x85fe ../shinonome/k12.8403 +0x8602 0x87fe ../shinonome/k12.8602 +0x8805 0x89f8 ../shinonome/k12.8805 +0x8a00 0x8b9a ../shinonome/k12.8a00 +0x8c37 0x8dff ../shinonome/k12.8c37 +0x8e08 0x8ffd ../shinonome/k12.8e08 +0x9000 0x91ff ../shinonome/k12.9000 +0x920d 0x93e8 ../shinonome/k12.920d +0x9403 0x95e5 ../shinonome/k12.9403 +0x961c 0x97ff ../shinonome/k12.961c +0x9801 0x99ff ../shinonome/k12.9801 +0x9a01 0x9bf5 ../shinonome/k12.9a01 +0x9c04 0x9dfd ../shinonome/k12.9c04 +0x9e1a 0x9fa0 ../shinonome/k12.9e1a +0xFB00 0xFBFF 7x13.FB00 +0xFE00 0xFEFF 7x13.FE00 +0xFF00 0xFFFF 7x13.FF00