font/sfnt: add a ppem arg to Font.LoadGlyph.

This lets us load a glyph at e.g. 12 pixels per em.

Change-Id: I048b3db89af8670782953a8361afe0e6373df9b0
Reviewed-on: https://go-review.googlesource.com/37175
Reviewed-by: David Crawshaw <crawshaw@golang.org>
This commit is contained in:
Nigel Tao 2017-02-18 13:13:36 +11:00
parent 791b615328
commit ed91dc314e
6 changed files with 280 additions and 32 deletions

125
font/sfnt/example_test.go Normal file
View File

@ -0,0 +1,125 @@
// Copyright 2017 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 sfnt_test
import (
"image"
"image/draw"
"log"
"os"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
"golang.org/x/image/vector"
)
func ExampleRasterizeGlyph() {
const (
ppem = 32
width = 24
height = 32
originX = 0
originY = 28
)
f, err := sfnt.Parse(goregular.TTF)
if err != nil {
log.Fatalf("Parse: %v", err)
}
var b sfnt.Buffer
x, err := f.GlyphIndex(&b, 'G')
if err != nil {
log.Fatalf("GlyphIndex: %v", err)
}
if x == 0 {
log.Fatalf("GlyphIndex: no glyph index found for the rune 'G'")
}
segments, err := f.LoadGlyph(&b, x, fixed.I(ppem), nil)
r := vector.NewRasterizer(width, height)
r.DrawOp = draw.Src
for _, seg := range segments {
// The divisions by 64 below is because the seg.Args values have type
// fixed.Int26_6, a 26.6 fixed point number, and 1<<6 == 64.
switch seg.Op {
case sfnt.SegmentOpMoveTo:
r.MoveTo(
originX+float32(seg.Args[0])/64,
originY-float32(seg.Args[1])/64,
)
case sfnt.SegmentOpLineTo:
r.LineTo(
originX+float32(seg.Args[0])/64,
originY-float32(seg.Args[1])/64,
)
case sfnt.SegmentOpQuadTo:
r.QuadTo(
originX+float32(seg.Args[0])/64,
originY-float32(seg.Args[1])/64,
originX+float32(seg.Args[2])/64,
originY-float32(seg.Args[3])/64,
)
case sfnt.SegmentOpCubeTo:
r.CubeTo(
originX+float32(seg.Args[0])/64,
originY-float32(seg.Args[1])/64,
originX+float32(seg.Args[2])/64,
originY-float32(seg.Args[3])/64,
originX+float32(seg.Args[4])/64,
originY-float32(seg.Args[5])/64,
)
}
}
// TODO: call ClosePath? Once overall or once per contour (i.e. MoveTo)?
dst := image.NewAlpha(image.Rect(0, 0, width, height))
r.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
const asciiArt = ".++8"
buf := make([]byte, 0, height*(width+1))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
a := dst.AlphaAt(x, y).A
buf = append(buf, asciiArt[a>>6])
}
buf = append(buf, '\n')
}
os.Stdout.Write(buf)
// Output:
// ........................
// ........................
// ........................
// ........................
// ..........+++++++++.....
// .......+8888888888888+..
// ......8888888888888888..
// ....+8888+........++88..
// ....8888................
// ...8888.................
// ..+888+.................
// ..+888..................
// ..888+..................
// .+888+..................
// .+888...................
// .+888...................
// .+888...................
// .+888..........+++++++..
// .+888..........8888888..
// .+888+.........+++8888..
// ..888+............+888..
// ..8888............+888..
// ..+888+...........+888..
// ...8888+..........+888..
// ...+8888+.........+888..
// ....+88888+.......+888..
// .....+8888888888888888..
// .......+888888888888++..
// ..........++++++++......
// ........................
// ........................
// ........................
}

View File

@ -653,8 +653,8 @@ func t2CAppendMoveto(p *psInterpreter) {
p.type2Charstrings.segments = append(p.type2Charstrings.segments, Segment{
Op: SegmentOpMoveTo,
Args: [6]fixed.Int26_6{
0: fixed.Int26_6(p.type2Charstrings.x) << 6,
1: fixed.Int26_6(p.type2Charstrings.y) << 6,
0: fixed.Int26_6(p.type2Charstrings.x),
1: fixed.Int26_6(p.type2Charstrings.y),
},
})
}
@ -663,8 +663,8 @@ func t2CAppendLineto(p *psInterpreter) {
p.type2Charstrings.segments = append(p.type2Charstrings.segments, Segment{
Op: SegmentOpLineTo,
Args: [6]fixed.Int26_6{
0: fixed.Int26_6(p.type2Charstrings.x) << 6,
1: fixed.Int26_6(p.type2Charstrings.y) << 6,
0: fixed.Int26_6(p.type2Charstrings.x),
1: fixed.Int26_6(p.type2Charstrings.y),
},
})
}
@ -685,12 +685,12 @@ func t2CAppendCubeto(p *psInterpreter, dxa, dya, dxb, dyb, dxc, dyc int32) {
p.type2Charstrings.segments = append(p.type2Charstrings.segments, Segment{
Op: SegmentOpCubeTo,
Args: [6]fixed.Int26_6{
0: fixed.Int26_6(xa) << 6,
1: fixed.Int26_6(ya) << 6,
2: fixed.Int26_6(xb) << 6,
3: fixed.Int26_6(yb) << 6,
4: fixed.Int26_6(xc) << 6,
5: fixed.Int26_6(yc) << 6,
0: fixed.Int26_6(xa),
1: fixed.Int26_6(ya),
2: fixed.Int26_6(xb),
3: fixed.Int26_6(yb),
4: fixed.Int26_6(xc),
5: fixed.Int26_6(yc),
},
})
}

View File

@ -35,6 +35,8 @@ import (
"io/ioutil"
"path/filepath"
"testing"
"golang.org/x/image/math/fixed"
)
var (
@ -132,6 +134,7 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi
if err != nil {
t.Fatalf("Parse: %v", err)
}
ppem := fixed.Int26_6(f.UnitsPerEm()) << 6
numGlyphs := f.NumGlyphs()
if numGlyphs < minNumGlyphs {
@ -144,7 +147,7 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi
iMax = firstUnsupportedGlyph
}
for i, numErrors := 0, 0; i < iMax; i++ {
if _, err := f.LoadGlyph(&buf, GlyphIndex(i), nil); err != nil {
if _, err := f.LoadGlyph(&buf, GlyphIndex(i), ppem, nil); err != nil {
t.Errorf("LoadGlyph(%d): %v", i, err)
numErrors++
}

View File

@ -131,6 +131,17 @@ const (
// display resolution (DPI) and font size (e.g. a 12 point font).
type Units int32
// scale returns x divided by unitsPerEm, rounded to the nearest fixed.Int26_6
// value (1/64th of a pixel).
func scale(x fixed.Int26_6, unitsPerEm Units) fixed.Int26_6 {
if x >= 0 {
x += fixed.Int26_6(unitsPerEm) / 2
} else {
x -= fixed.Int26_6(unitsPerEm) / 2
}
return x / fixed.Int26_6(unitsPerEm)
}
func u16(b []byte) uint16 {
_ = b[1] // Bounds check hint to compiler.
return uint16(b[0])<<8 | uint16(b[1])<<0
@ -274,6 +285,12 @@ func ParseReaderAt(src io.ReaderAt) (*Font, error) {
//
// The Font methods that don't take a *Buffer argument are always safe to call
// concurrently.
//
// Some methods provide lengths or co-ordinates, e.g. bounds, font metrics and
// control points. All of these methods take a ppem parameter, which is the
// number of pixels in 1 em, expressed as a 26.6 fixed point value. For
// example, if 1 em is 10 pixels then ppem is fixed.I(10), which equals
// fixed.Int26_6(10 << 6).
type Font struct {
src source
@ -623,15 +640,16 @@ func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) ([]byte, error) {
// LoadGlyphOptions are the options to the Font.LoadGlyph method.
type LoadGlyphOptions struct {
// TODO: scale / transform / hinting.
// TODO: transform / hinting.
}
// LoadGlyph returns the vector segments for the x'th glyph.
// LoadGlyph returns the vector segments for the x'th glyph. ppem is the number
// of pixels in 1 em.
//
// If b is non-nil, the segments become invalid to use once b is re-used.
//
// It returns ErrNotFound if the glyph index is out of range.
func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, opts *LoadGlyphOptions) ([]Segment, error) {
func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, opts *LoadGlyphOptions) ([]Segment, error) {
if b == nil {
b = &Buffer{}
}
@ -656,7 +674,19 @@ func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, opts *LoadGlyphOptions) ([]Seg
b.segments = segments
}
// TODO: look at opts to scale / transform / hint the Buffer.segments.
// Scale the segments. If we want to support hinting, we'll have to push
// the scaling computation into the PostScript / TrueType specific glyph
// loading code, such as the appendGlyfSegments body, since TrueType
// hinting bytecode works on the scaled glyph vectors. For now, though,
// it's simpler to scale as a post-processing step.
for i := range b.segments {
s := &b.segments[i]
for j := range s.Args {
s.Args[j] = scale(s.Args[j]*ppem, f.cached.unitsPerEm)
}
}
// TODO: look at opts to transform / hint the Buffer.segments.
return b.segments, nil
}

View File

@ -6,6 +6,7 @@ package sfnt
import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"testing"
@ -24,6 +25,16 @@ func moveTo(xa, ya int) Segment {
}
}
func moveTo26_6(xa, ya fixed.Int26_6) Segment {
return Segment{
Op: SegmentOpMoveTo,
Args: [6]fixed.Int26_6{
0: xa,
1: ya,
},
}
}
func lineTo(xa, ya int) Segment {
return Segment{
Op: SegmentOpLineTo,
@ -34,6 +45,16 @@ func lineTo(xa, ya int) Segment {
}
}
func lineTo26_6(xa, ya fixed.Int26_6) Segment {
return Segment{
Op: SegmentOpLineTo,
Args: [6]fixed.Int26_6{
0: xa,
1: ya,
},
}
}
func quadTo(xa, ya, xb, yb int) Segment {
return Segment{
Op: SegmentOpQuadTo,
@ -60,6 +81,20 @@ func cubeTo(xa, ya, xb, yb, xc, yc int) Segment {
}
}
func checkSegmentsEqual(got, want []Segment) error {
if len(got) != len(want) {
return fmt.Errorf("got %d elements, want %d\noverall:\ngot %v\nwant %v",
len(got), len(want), got, want)
}
for i, g := range got {
if w := want[i]; g != w {
return fmt.Errorf("element %d:\ngot %v\nwant %v\noverall:\ngot %v\nwant %v",
i, g, w, got, want)
}
}
return nil
}
func TestTrueTypeParse(t *testing.T) {
f, err := Parse(goregular.TTF)
if err != nil {
@ -389,38 +424,30 @@ func TestTrueTypeSegments(t *testing.T) {
func testSegments(t *testing.T, filename string, wants [][]Segment) {
data, err := ioutil.ReadFile(filepath.FromSlash("../testdata/" + filename))
if err != nil {
t.Fatal(err)
t.Fatalf("ReadFile: %v", err)
}
f, err := Parse(data)
if err != nil {
t.Fatal(err)
t.Fatalf("Parse: %v", err)
}
ppem := fixed.Int26_6(f.UnitsPerEm()) << 6
if ng := f.NumGlyphs(); ng != len(wants) {
t.Fatalf("NumGlyphs: got %d, want %d", ng, len(wants))
}
var b Buffer
loop:
for i, want := range wants {
got, err := f.LoadGlyph(&b, GlyphIndex(i), nil)
got, err := f.LoadGlyph(&b, GlyphIndex(i), ppem, nil)
if err != nil {
t.Errorf("i=%d: LoadGlyph: %v", i, err)
continue
}
if len(got) != len(want) {
t.Errorf("i=%d: got %d elements, want %d\noverall:\ngot %v\nwant %v",
i, len(got), len(want), got, want)
if err := checkSegmentsEqual(got, want); err != nil {
t.Errorf("i=%d: %v", i, err)
continue
}
for j, g := range got {
if w := want[j]; g != w {
t.Errorf("i=%d: element %d:\ngot %v\nwant %v\noverall:\ngot %v\nwant %v",
i, j, g, w, got, want)
continue loop
}
}
}
if _, err := f.LoadGlyph(nil, 0xffff, nil); err != ErrNotFound {
if _, err := f.LoadGlyph(nil, 0xffff, ppem, nil); err != ErrNotFound {
t.Errorf("LoadGlyph(..., 0xffff, ...):\ngot %v\nwant %v", err, ErrNotFound)
}
@ -432,6 +459,60 @@ loop:
}
}
func TestPPEM(t *testing.T) {
data, err := ioutil.ReadFile(filepath.FromSlash("../testdata/glyfTest.ttf"))
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
f, err := Parse(data)
if err != nil {
t.Fatalf("Parse: %v", err)
}
var b Buffer
x, err := f.GlyphIndex(&b, '1')
if err != nil {
t.Fatalf("GlyphIndex: %v", err)
}
if x == 0 {
t.Fatalf("GlyphIndex: no glyph index found for the rune '1'")
}
testCases := []struct {
ppem fixed.Int26_6
want []Segment
}{{
ppem: fixed.I(12),
want: []Segment{
moveTo26_6(77, 0),
lineTo26_6(77, 614),
lineTo26_6(230, 614),
lineTo26_6(230, 0),
lineTo26_6(77, 0),
},
}, {
ppem: fixed.I(2048),
want: []Segment{
moveTo(205, 0),
lineTo(205, 1638),
lineTo(614, 1638),
lineTo(614, 0),
lineTo(205, 0),
},
}}
for i, tc := range testCases {
got, err := f.LoadGlyph(&b, x, tc.ppem, nil)
if err != nil {
t.Errorf("i=%d: LoadGlyph: %v", i, err)
continue
}
if err := checkSegmentsEqual(got, tc.want); err != nil {
t.Errorf("i=%d: %v", i, err)
continue
}
}
}
func TestGlyphName(t *testing.T) {
f, err := Parse(goregular.TTF)
if err != nil {

View File

@ -357,9 +357,18 @@ func (g *glyfIter) nextSegment() (ok bool) {
return true
}
// Convert the tuple (g.x, g.y) to a fixed.Point26_6, since the latter
// is what's held in a Segment. The input (g.x, g.y) is a pair of int16
// values, measured in font units, since that is what the underlying
// format provides. The output is a pair of fixed.Int26_6 values. A
// fixed.Int26_6 usually represents a 26.6 fixed number of pixels, but
// this here is just a straight numerical conversion, with no scaling
// factor. A later step scales the Segment.Args values by such a factor
// to convert e.g. 1792 font units to 10.5 pixels at 2048 font units
// per em and 12 ppem (pixels per em).
p := fixed.Point26_6{
X: fixed.Int26_6(g.x) << 6,
Y: fixed.Int26_6(g.y) << 6,
X: fixed.Int26_6(g.x),
Y: fixed.Int26_6(g.y),
}
if !g.firstOnCurveValid {