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 <r@golang.org>
This commit is contained in:
parent
8ca9b58be9
commit
eecb4e626f
84
example/font/main.go
Normal file
84
example/font/main.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
114
font/font.go
Normal file
114
font/font.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
388
font/plan9font/plan9font.go
Normal file
388
font/plan9font/plan9font.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user