From 6baa5f0a466e96a2061796a9ca7441de723f359d Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Sat, 16 Jun 2012 12:19:07 +1000 Subject: [PATCH] freetype: optimize []byte to uint16/uint32 conversions. Thanks to Jeff R. Allen for the conversation that led to this change. benchmark old ns/op new ns/op delta BenchmarkDrawString 21168440 20143860 -4.84% The number of mallocs per iteration is unchanged. R=rsc, r CC=golang-dev, jra http://codereview.appspot.com/6304077 --- freetype/freetype_test.go | 59 ++++++++++++ freetype/truetype/glyph.go | 85 +++++++++-------- freetype/truetype/truetype.go | 146 ++++++++++++----------------- freetype/truetype/truetype_test.go | 69 ++++++++++++++ 4 files changed, 234 insertions(+), 125 deletions(-) create mode 100644 freetype/freetype_test.go create mode 100644 freetype/truetype/truetype_test.go diff --git a/freetype/freetype_test.go b/freetype/freetype_test.go new file mode 100644 index 0000000..07294e8 --- /dev/null +++ b/freetype/freetype_test.go @@ -0,0 +1,59 @@ +// Copyright 2012 The Freetype-Go Authors. All rights reserved. +// Use of this source code is governed by your choice of either the +// FreeType License or the GNU General Public License version 2 (or +// any later version), both of which can be found in the LICENSE file. + +package freetype + +import ( + "image" + "image/draw" + "io/ioutil" + "runtime" + "strings" + "testing" +) + +func BenchmarkDrawString(b *testing.B) { + data, err := ioutil.ReadFile("../licenses/gpl.txt") + if err != nil { + b.Fatal(err) + } + lines := strings.Split(string(data), "\n") + + data, err = ioutil.ReadFile("../luxi-fonts/luxisr.ttf") + if err != nil { + b.Fatal(err) + } + font, err := ParseFont(data) + if err != nil { + b.Fatal(err) + } + + dst := image.NewRGBA(image.Rect(0, 0, 800, 600)) + draw.Draw(dst, dst.Bounds(), image.White, image.ZP, draw.Src) + + c := NewContext() + c.SetDst(dst) + c.SetClip(dst.Bounds()) + c.SetSrc(image.Black) + c.SetFont(font) + + var ms runtime.MemStats + runtime.ReadMemStats(&ms) + mallocs := ms.Mallocs + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j, line := range lines { + _, err := c.DrawString(line, Pt(0, (j*16)%600)) + if err != nil { + b.Fatal(err) + } + } + } + b.StopTimer() + runtime.ReadMemStats(&ms) + mallocs = ms.Mallocs - mallocs + b.Logf("%d iterations, %d mallocs per iteration\n", b.N, int(mallocs)/b.N) +} diff --git a/freetype/truetype/glyph.go b/freetype/truetype/glyph.go index cb5099a..9a75ed5 100644 --- a/freetype/truetype/glyph.go +++ b/freetype/truetype/glyph.go @@ -47,36 +47,40 @@ const ( // decodeFlags decodes a glyph's run-length encoded flags, // and returns the remaining data. -func (g *GlyphBuf) decodeFlags(d data, np0 int) data { +func (g *GlyphBuf) decodeFlags(d []byte, offset int, np0 int) (offset1 int) { for i := np0; i < len(g.Point); { - c := d.u8() + c := d[offset] + offset++ g.Point[i].Flags = c i++ if c&flagRepeat != 0 { - count := d.u8() + count := d[offset] + offset++ for ; count > 0; count-- { g.Point[i].Flags = c i++ } } } - return d + return offset } // decodeCoords decodes a glyph's delta encoded co-ordinates. -func (g *GlyphBuf) decodeCoords(d data, np0 int) { +func (g *GlyphBuf) decodeCoords(d []byte, offset int, np0 int) int { var x int16 for i := np0; i < len(g.Point); i++ { f := g.Point[i].Flags if f&flagXShortVector != 0 { - dx := int16(d.u8()) + dx := int16(d[offset]) + offset++ if f&flagPositiveXShortVector == 0 { x -= dx } else { x += dx } } else if f&flagThisXIsSame == 0 { - x += int16(d.u16()) + x += int16(u16(d, offset)) + offset += 2 } g.Point[i].X = x } @@ -84,17 +88,20 @@ func (g *GlyphBuf) decodeCoords(d data, np0 int) { for i := np0; i < len(g.Point); i++ { f := g.Point[i].Flags if f&flagYShortVector != 0 { - dy := int16(d.u8()) + dy := int16(d[offset]) + offset++ if f&flagPositiveYShortVector == 0 { y -= dy } else { y += dy } } else if f&flagThisYIsSame == 0 { - y += int16(d.u16()) + y += int16(u16(d, offset)) + offset += 2 } g.Point[i].Y = y } + return offset } // Load loads a glyph's contours from a Font, overwriting any previously @@ -102,13 +109,13 @@ func (g *GlyphBuf) decodeCoords(d data, np0 int) { func (g *GlyphBuf) Load(f *Font, i Index) error { // Reset the GlyphBuf. g.B = Bounds{} - g.Point = g.Point[0:0] - g.End = g.End[0:0] + g.Point = g.Point[:0] + g.End = g.End[:0] return g.load(f, i, 0) } // loadCompound loads a glyph that is composed of other glyphs. -func (g *GlyphBuf) loadCompound(f *Font, d data, recursion int) error { +func (g *GlyphBuf) loadCompound(f *Font, glyf []byte, offset, recursion int) error { // Flags for decoding a compound glyph. These flags are documented at // http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html. const ( @@ -125,15 +132,17 @@ func (g *GlyphBuf) loadCompound(f *Font, d data, recursion int) error { flagOverlapCompound ) for { - flags := d.u16() - component := d.u16() + flags := u16(glyf, offset) + component := u16(glyf, offset+2) var dx, dy int16 if flags&flagArg1And2AreWords != 0 { - dx = int16(d.u16()) - dy = int16(d.u16()) + dx = int16(u16(glyf, offset+4)) + dy = int16(u16(glyf, offset+6)) + offset += 8 } else { - dx = int16(int8(d.u8())) - dy = int16(int8(d.u8())) + dx = int16(int8(glyf[offset+4])) + dy = int16(int8(glyf[offset+5])) + offset += 6 } if flags&flagArgsAreXYValues == 0 { return UnsupportedError("compound glyph transform vector") @@ -165,26 +174,25 @@ func (g *GlyphBuf) load(f *Font, i Index, recursion int) error { // Find the relevant slice of f.glyf. var g0, g1 uint32 if f.locaOffsetFormat == locaOffsetFormatShort { - d := data(f.loca[2*int(i):]) - g0 = 2 * uint32(d.u16()) - g1 = 2 * uint32(d.u16()) + g0 = 2 * uint32(u16(f.loca, 2*int(i))) + g1 = 2 * uint32(u16(f.loca, 2*int(i)+2)) } else { - d := data(f.loca[4*int(i):]) - g0 = d.u32() - g1 = d.u32() + g0 = u32(f.loca, 4*int(i)) + g1 = u32(f.loca, 4*int(i)+4) } if g0 == g1 { return nil } - d := data(f.glyf[g0:g1]) + glyf := f.glyf[g0:g1] // Decode the contour end indices. - ne := int(int16(d.u16())) - g.B.XMin = int16(d.u16()) - g.B.YMin = int16(d.u16()) - g.B.XMax = int16(d.u16()) - g.B.YMax = int16(d.u16()) + ne := int(int16(u16(glyf, 0))) + g.B.XMin = int16(u16(glyf, 2)) + g.B.YMin = int16(u16(glyf, 4)) + g.B.XMax = int16(u16(glyf, 6)) + g.B.YMax = int16(u16(glyf, 8)) + offset := 10 if ne == -1 { - return g.loadCompound(f, d, recursion) + return g.loadCompound(f, glyf, offset, recursion) } else if ne < 0 { // http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html says that // "the values -2, -3, and so forth, are reserved for future use." @@ -193,25 +201,26 @@ func (g *GlyphBuf) load(f *Font, i Index, recursion int) error { ne0, np0 := len(g.End), len(g.Point) ne += ne0 if ne <= cap(g.End) { - g.End = g.End[0:ne] + g.End = g.End[:ne] } else { g.End = make([]int, ne, ne*2) } for i := ne0; i < ne; i++ { - g.End[i] = 1 + np0 + int(d.u16()) + g.End[i] = 1 + np0 + int(u16(glyf, offset)) + offset += 2 } // Skip the TrueType hinting instructions. - instrLen := int(d.u16()) - d.skip(instrLen) + instrLen := int(u16(glyf, offset)) + offset += 2 + instrLen // Decode the points. np := int(g.End[ne-1]) if np <= cap(g.Point) { - g.Point = g.Point[0:np] + g.Point = g.Point[:np] } else { g.Point = make([]Point, np, np*2) } - d = g.decodeFlags(d, np0) - g.decodeCoords(d, np0) + offset = g.decodeFlags(glyf, offset, np0) + g.decodeCoords(glyf, offset, np0) return nil } diff --git a/freetype/truetype/truetype.go b/freetype/truetype/truetype.go index 313f9ca..5204a3e 100644 --- a/freetype/truetype/truetype.go +++ b/freetype/truetype/truetype.go @@ -47,43 +47,23 @@ func (e UnsupportedError) Error() string { return "freetype: unsupported TrueType feature: " + string(e) } -// data interprets a byte slice as a stream of integer values. -type data []byte - -// u32 returns the next big-endian uint32. -func (d *data) u32() uint32 { - x := uint32((*d)[0])<<24 | uint32((*d)[1])<<16 | uint32((*d)[2])<<8 | uint32((*d)[3]) - *d = (*d)[4:] - return x +// u32 returns the big-endian uint32 at b[i:]. +func u32(b []byte, i int) uint32 { + return uint32(b[i])<<24 | uint32(b[i+1])<<16 | uint32(b[i+2])<<8 | uint32(b[i+3]) } -// u16 returns the next big-endian uint16. -func (d *data) u16() uint16 { - x := uint16((*d)[0])<<8 | uint16((*d)[1]) - *d = (*d)[2:] - return x -} - -// u8 returns the next uint8. -func (d *data) u8() uint8 { - x := (*d)[0] - *d = (*d)[1:] - return x -} - -// skip skips the next n bytes. -func (d *data) skip(n int) { - *d = (*d)[n:] +// u16 returns the big-endian uint16 at b[i:]. +func u16(b []byte, i int) uint16 { + return uint16(b[i])<<8 | uint16(b[i+1]) } // readTable returns a slice of the TTF data given by a table's directory entry. func readTable(ttf []byte, offsetLength []byte) ([]byte, error) { - d := data(offsetLength) - offset := int(d.u32()) + offset := int(u32(offsetLength, 0)) if offset < 0 { return nil, FormatError(fmt.Sprintf("offset too large: %d", uint32(offset))) } - length := int(d.u32()) + length := int(u32(offsetLength, 4)) if length < 0 { return nil, FormatError(fmt.Sprintf("length too large: %d", uint32(length))) } @@ -134,16 +114,16 @@ func (f *Font) parseCmap() error { if len(f.cmap) < 4 { return FormatError("cmap too short") } - d := data(f.cmap[2:]) - nsubtab := int(d.u16()) + nsubtab := int(u16(f.cmap, 2)) if len(f.cmap) < 8*nsubtab+4 { return FormatError("cmap too short") } - offset, found := 0, false + offset, found, x := 0, false, 4 for i := 0; i < nsubtab; i++ { // We read the 16-bit Platform ID and 16-bit Platform Specific ID as a single uint32. // All values are big-endian. - pidPsid, o := d.u32(), d.u32() + pidPsid, o := u32(f.cmap, x), u32(f.cmap, x+4) + x += 8 // We prefer the Unicode cmap encoding. Failing to find that, we fall // back onto the Microsoft cmap encoding. if pidPsid == unicodeEncoding { @@ -161,37 +141,39 @@ func (f *Font) parseCmap() error { return FormatError("bad cmap offset") } - d = data(f.cmap[offset:]) - cmapFormat := d.u16() + cmapFormat := u16(f.cmap, offset) if cmapFormat != cmapFormat4 { return UnsupportedError(fmt.Sprintf("cmap format: %d", cmapFormat)) } - d.skip(2) - language := d.u16() + language := u16(f.cmap, offset+4) if language != languageIndependent { return UnsupportedError(fmt.Sprintf("language: %d", language)) } - segCountX2 := int(d.u16()) + segCountX2 := int(u16(f.cmap, offset+6)) if segCountX2%2 == 1 { return FormatError(fmt.Sprintf("bad segCountX2: %d", segCountX2)) } segCount := segCountX2 / 2 - d.skip(6) + offset += 14 f.cm = make([]cm, segCount) for i := 0; i < segCount; i++ { - f.cm[i].end = d.u16() + f.cm[i].end = u16(f.cmap, offset) + offset += 2 } - d.skip(2) + offset += 2 for i := 0; i < segCount; i++ { - f.cm[i].start = d.u16() + f.cm[i].start = u16(f.cmap, offset) + offset += 2 } for i := 0; i < segCount; i++ { - f.cm[i].delta = d.u16() + f.cm[i].delta = u16(f.cmap, offset) + offset += 2 } for i := 0; i < segCount; i++ { - f.cm[i].offset = d.u16() + f.cm[i].offset = u16(f.cmap, offset) + offset += 2 } - f.cmapIndexes = []byte(d) + f.cmapIndexes = f.cmap[offset:] return nil } @@ -199,15 +181,12 @@ func (f *Font) parseHead() error { if len(f.head) != 54 { return FormatError(fmt.Sprintf("bad head length: %d", len(f.head))) } - d := data(f.head[18:]) - f.unitsPerEm = int(d.u16()) - d.skip(16) - f.bounds.XMin = int16(d.u16()) - f.bounds.YMin = int16(d.u16()) - f.bounds.XMax = int16(d.u16()) - f.bounds.YMax = int16(d.u16()) - d.skip(6) - switch i := d.u16(); i { + f.unitsPerEm = int(u16(f.head, 18)) + f.bounds.XMin = int16(u16(f.head, 36)) + f.bounds.YMin = int16(u16(f.head, 38)) + f.bounds.XMax = int16(u16(f.head, 40)) + f.bounds.YMax = int16(u16(f.head, 42)) + switch i := u16(f.head, 50); i { case 0: f.locaOffsetFormat = locaOffsetFormatShort case 1: @@ -222,8 +201,7 @@ func (f *Font) parseHhea() error { if len(f.hhea) != 36 { return FormatError(fmt.Sprintf("bad hhea length: %d", len(f.hhea))) } - d := data(f.hhea[34:]) - f.nHMetric = int(d.u16()) + f.nHMetric = int(u16(f.hhea, 34)) if 4*f.nHMetric+2*(f.nGlyph-f.nHMetric) != len(f.hmtx) { return FormatError(fmt.Sprintf("bad hmtx length: %d", len(f.hmtx))) } @@ -249,23 +227,22 @@ func (f *Font) parseKern() error { if len(f.kern) < 18 { return FormatError("kern data too short") } - d := data(f.kern[0:]) - version := d.u16() + version, offset := u16(f.kern, 0), 2 if version != 0 { return UnsupportedError(fmt.Sprintf("kern version: %d", version)) } - n := d.u16() + n, offset := u16(f.kern, offset), offset+2 if n != 1 { return UnsupportedError(fmt.Sprintf("kern nTables: %d", n)) } - d.skip(2) - length := int(d.u16()) - coverage := d.u16() + offset += 2 + length, offset := int(u16(f.kern, offset)), offset+2 + coverage, offset := u16(f.kern, offset), offset+2 if coverage != 0x0001 { // We only support horizontal kerning. return UnsupportedError(fmt.Sprintf("kern coverage: 0x%04x", coverage)) } - f.nKern = int(d.u16()) + f.nKern, offset = int(u16(f.kern, offset)), offset+2 if 6*f.nKern != length-14 { return FormatError("bad kern table length") } @@ -276,8 +253,7 @@ func (f *Font) parseMaxp() error { if len(f.maxp) != 32 { return FormatError(fmt.Sprintf("bad maxp length: %d", len(f.maxp))) } - d := data(f.maxp[4:]) - f.nGlyph = int(d.u16()) + f.nGlyph = int(u16(f.maxp, 4)) return nil } @@ -301,11 +277,10 @@ func (f *Font) Index(x rune) Index { return Index(c + f.cm[i].delta) } offset := int(f.cm[i].offset) + 2*(i-n+int(c-f.cm[i].start)) - d := data(f.cmapIndexes[offset:]) - return Index(d.u16()) + return Index(u16(f.cmapIndexes, offset)) } } - return Index(0) + return 0 } // HMetric returns the horizontal metrics for the glyph with the given index. @@ -315,17 +290,13 @@ func (f *Font) HMetric(i Index) HMetric { return HMetric{} } if j >= f.nHMetric { - var hm HMetric p := 4 * (f.nHMetric - 1) - d := data(f.hmtx[p:]) - hm.AdvanceWidth = d.u16() - p += 2*(j-f.nHMetric) + 4 - d = data(f.hmtx[p:]) - hm.LeftSideBearing = int16(d.u16()) - return hm + return HMetric{ + u16(f.hmtx, p), + int16(u16(f.hmtx, p+2*(j-f.nHMetric)+4)), + } } - d := data(f.hmtx[4*j:]) - return HMetric{d.u16(), int16(d.u16())} + return HMetric{u16(f.hmtx, 4*j), int16(u16(f.hmtx, 4*j+2))} } // Kerning returns the kerning for the given glyph pair. @@ -337,14 +308,13 @@ func (f *Font) Kerning(i0, i1 Index) int16 { lo, hi := 0, f.nKern for lo < hi { i := (lo + hi) / 2 - d := data(f.kern[18+6*i:]) - ig := d.u32() + ig := u32(f.kern, 18+6*i) if ig < g { lo = i + 1 } else if ig > g { hi = i } else { - return int16(d.u16()) + return int16(u16(f.kern, 22+6*i)) } } return 0 @@ -362,33 +332,35 @@ func parse(ttf []byte, offset int) (font *Font, err error) { err = FormatError("TTF data is too short") return } - d := data(ttf[offset:]) - switch d.u32() { + originalOffset := offset + magic, offset := u32(ttf, offset), offset+4 + switch magic { case 0x00010000: // No-op. case 0x74746366: // "ttcf" as a big-endian uint32. - if offset != 0 { + if originalOffset != 0 { err = FormatError("recursive TTC") return } - if d.u32() != 0x00010000 { + ttcVersion, offset := u32(ttf, offset), offset+4 + if ttcVersion != 0x00010000 { // TODO: support TTC version 2.0, once I have such a .ttc file to test with. err = FormatError("bad TTC version") return } - numFonts := int(d.u32()) + numFonts, offset := int(u32(ttf, offset)), offset+4 if numFonts <= 0 { err = FormatError("bad number of TTC fonts") return } - if len(d)/4 < numFonts { + if len(ttf[offset:])/4 < numFonts { err = FormatError("TTC offset table is too short") return } // TODO: provide an API to select which font in a TrueType collection to return, // not just the first one. This may require an API to parse a TTC's name tables, // so users of this package can select the font in a TTC by name. - offset := int(d.u32()) + offset = int(u32(ttf, offset)) if offset <= 0 || offset > len(ttf) { err = FormatError("bad TTC offset") return @@ -398,7 +370,7 @@ func parse(ttf []byte, offset int) (font *Font, err error) { err = FormatError("bad TTF version") return } - n := int(d.u16()) + n, offset := int(u16(ttf, offset)), offset+2 if len(ttf) < 16*n+12 { err = FormatError("TTF data is too short") return diff --git a/freetype/truetype/truetype_test.go b/freetype/truetype/truetype_test.go new file mode 100644 index 0000000..b861a36 --- /dev/null +++ b/freetype/truetype/truetype_test.go @@ -0,0 +1,69 @@ +// Copyright 2012 The Freetype-Go Authors. All rights reserved. +// Use of this source code is governed by your choice of either the +// FreeType License or the GNU General Public License version 2 (or +// any later version), both of which can be found in the LICENSE file. + +package truetype + +import ( + "fmt" + "io/ioutil" + "testing" +) + +// TestParse tests that the luxisr.ttf metrics and glyphs are parsed correctly. +// The numerical values can be manually verified by examining luxisr.ttx. +func TestParse(t *testing.T) { + b, err := ioutil.ReadFile("../../luxi-fonts/luxisr.ttf") + if err != nil { + t.Fatal(err) + } + font, err := Parse(b) + if err != nil { + t.Fatal(err) + } + if got, want := font.Bounds(), (Bounds{-441, -432, 2024, 2033}); got != want { + t.Errorf("Bounds: got %v, want %v", got, want) + } + if got, want := font.UnitsPerEm(), 2048; got != want { + t.Errorf("UnitsPerEm: got %v, want %v", got, want) + } + + i0 := font.Index('A') + i1 := font.Index('V') + if i0 != 36 || i1 != 57 { + t.Fatalf("Index: i0, i1 = %d, %d, want 36, 57", i0, i1) + } + if got, want := font.HMetric(i0), (HMetric{1366, 19}); got != want { + t.Errorf("HMetric: got %v, want %v", got, want) + } + if got, want := font.Kerning(i0, i1), int16(-144); got != want { + t.Errorf("Kerning: got %v, want %v", got, want) + } + + g0 := NewGlyphBuf() + err = g0.Load(font, i0) + if err != nil { + t.Fatalf("Load: %v", err) + } + g1 := &GlyphBuf{ + B: Bounds{19, 0, 1342, 1480}, + Point: []Point{ + {19, 0, 51}, + {581, 1480, 1}, + {789, 1480, 51}, + {1342, 0, 1}, + {1116, 0, 35}, + {962, 410, 3}, + {368, 410, 33}, + {214, 0, 3}, + {428, 566, 19}, + {904, 566, 33}, + {667, 1200, 3}, + }, + End: []int{8, 11}, + } + if got, want := fmt.Sprint(g0), fmt.Sprint(g1); got != want { + t.Errorf("GlyphBuf:\ngot %v\nwant %v", got, want) + } +}