From 10ed2942051107bfc4d4a9370c12bf737e94bc9a Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Sat, 1 Apr 2017 15:09:28 +1100 Subject: [PATCH] font/sfnt: implement Font.GlyphAdvance. Change-Id: I3e15c6e634d858a87e73221bd9d5a9e3979d674a Reviewed-on: https://go-review.googlesource.com/39250 Reviewed-by: David Crawshaw --- font/sfnt/postscript.go | 8 ++--- font/sfnt/sfnt.go | 80 ++++++++++++++++++++++++++++++++++++++--- font/sfnt/sfnt_test.go | 71 ++++++++++++++++++++++++++++++++++++ font/sfnt/truetype.go | 2 +- 4 files changed, 151 insertions(+), 10 deletions(-) diff --git a/font/sfnt/postscript.go b/font/sfnt/postscript.go index 2f746c4..f166b6c 100644 --- a/font/sfnt/postscript.go +++ b/font/sfnt/postscript.go @@ -132,7 +132,7 @@ type cffParser struct { psi psInterpreter } -func (p *cffParser) parse(numGlyphs int) (ret glyphData, err error) { +func (p *cffParser) parse(numGlyphs int32) (ret glyphData, err error) { // Parse the header. { if !p.read(4) { @@ -236,7 +236,7 @@ func (p *cffParser) parse(numGlyphs int) (ret glyphData, err error) { if !ok { return glyphData{}, p.err } - if count == 0 || int(count) != numGlyphs { + if count == 0 || int32(count) != numGlyphs { return glyphData{}, errInvalidCFFTable } ret.locations = make([]uint32, count+1) @@ -312,7 +312,7 @@ func (p *cffParser) parse(numGlyphs int) (ret glyphData, err error) { // parseFDSelect parses the Font Dict Select data as per 5176.CFF.pdf section // 19 "FDSelect". -func (p *cffParser) parseFDSelect(offset int32, numGlyphs int) (ret fdSelect, err error) { +func (p *cffParser) parseFDSelect(offset int32, numGlyphs int32) (ret fdSelect, err error) { if !p.seekFromBase(p.psi.topDict.fdSelect) { return fdSelect{}, errInvalidCFFTable } @@ -322,7 +322,7 @@ func (p *cffParser) parseFDSelect(offset int32, numGlyphs int) (ret fdSelect, er ret.format = p.buf[0] switch ret.format { case 0: - if p.end-p.offset < numGlyphs { + if p.end-p.offset < int(numGlyphs) { return fdSelect{}, errInvalidCFFTable } ret.offset = int32(p.offset) diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go index 6668606..f177e82 100644 --- a/font/sfnt/sfnt.go +++ b/font/sfnt/sfnt.go @@ -75,6 +75,8 @@ var ( errInvalidGlyphData = errors.New("sfnt: invalid glyph data") errInvalidGlyphDataLength = errors.New("sfnt: invalid glyph data length") errInvalidHeadTable = errors.New("sfnt: invalid head table") + errInvalidHheaTable = errors.New("sfnt: invalid hhea table") + errInvalidHmtxTable = errors.New("sfnt: invalid hmtx table") errInvalidKernTable = errors.New("sfnt: invalid kern table") errInvalidLocaTable = errors.New("sfnt: invalid loca table") errInvalidLocationData = errors.New("sfnt: invalid location data") @@ -447,6 +449,7 @@ type Font struct { isPostScript bool kernNumPairs int32 kernOffset int32 + numHMetrics int32 postTableVersion uint32 unitsPerEm Units } @@ -495,6 +498,14 @@ func (f *Font) initialize(offset int) error { if err != nil { return err } + buf, numHMetrics, err := f.parseHhea(buf, numGlyphs) + if err != nil { + return err + } + buf, err = f.parseHmtx(buf, numGlyphs, numHMetrics) + if err != nil { + return err + } buf, postTableVersion, err := f.parsePost(buf, numGlyphs) if err != nil { return err @@ -506,6 +517,7 @@ func (f *Font) initialize(offset int) error { f.cached.isPostScript = isPostScript f.cached.kernNumPairs = kernNumPairs f.cached.kernOffset = kernOffset + f.cached.numHMetrics = numHMetrics f.cached.postTableVersion = postTableVersion f.cached.unitsPerEm = unitsPerEm @@ -678,6 +690,31 @@ func (f *Font) parseHead(buf []byte) (buf1 []byte, indexToLocFormat bool, unitsP return buf, indexToLocFormat, unitsPerEm, nil } +func (f *Font) parseHhea(buf []byte, numGlyphs int32) (buf1 []byte, numHMetrics int32, err error) { + // https://www.microsoft.com/typography/OTSPEC/hhea.htm + + if f.hhea.length != 36 { + return nil, 0, errInvalidHheaTable + } + u, err := f.src.u16(buf, f.hhea, 34) + if err != nil { + return nil, 0, err + } + if int32(u) > numGlyphs || u == 0 { + return nil, 0, errInvalidHheaTable + } + return buf, int32(u), nil +} + +func (f *Font) parseHmtx(buf []byte, numGlyphs, numHMetrics int32) (buf1 []byte, err error) { + // https://www.microsoft.com/typography/OTSPEC/hmtx.htm + + if f.hmtx.length != uint32(2*numGlyphs+2*numHMetrics) { + return nil, errInvalidHmtxTable + } + return buf, nil +} + func (f *Font) parseKern(buf []byte) (buf1 []byte, kernNumPairs, kernOffset int32, err error) { // https://www.microsoft.com/typography/otspec/kern.htm @@ -772,7 +809,7 @@ func (f *Font) parseKernFormat0(buf []byte, offset, length int) (buf1 []byte, ke return buf, kernNumPairs, int32(offset) + headerSize, nil } -func (f *Font) parseMaxp(buf []byte, isPostScript bool) (buf1 []byte, numGlyphs int, err error) { +func (f *Font) parseMaxp(buf []byte, isPostScript bool) (buf1 []byte, numGlyphs int32, err error) { // https://www.microsoft.com/typography/otspec/maxp.htm if isPostScript { @@ -788,7 +825,7 @@ func (f *Font) parseMaxp(buf []byte, isPostScript bool) (buf1 []byte, numGlyphs if err != nil { return nil, 0, err } - return buf, int(u), nil + return buf, int32(u), nil } type glyphData struct { @@ -809,7 +846,7 @@ type glyphData struct { fdSelect fdSelect } -func (f *Font) parseGlyphData(buf []byte, numGlyphs int, indexToLocFormat, isPostScript bool) (buf1 []byte, ret glyphData, err error) { +func (f *Font) parseGlyphData(buf []byte, numGlyphs int32, indexToLocFormat, isPostScript bool) (buf1 []byte, ret glyphData, err error) { if isPostScript { p := cffParser{ src: &f.src, @@ -827,14 +864,14 @@ func (f *Font) parseGlyphData(buf []byte, numGlyphs int, indexToLocFormat, isPos return nil, glyphData{}, err } } - if len(ret.locations) != numGlyphs+1 { + if len(ret.locations) != int(numGlyphs+1) { return nil, glyphData{}, errInvalidLocationData } return buf, ret, nil } -func (f *Font) parsePost(buf []byte, numGlyphs int) (buf1 []byte, postTableVersion uint32, err error) { +func (f *Font) parsePost(buf []byte, numGlyphs int32) (buf1 []byte, postTableVersion uint32, err error) { // https://www.microsoft.com/typography/otspec/post.htm const headerSize = 32 @@ -1022,6 +1059,39 @@ func (f *Font) GlyphName(b *Buffer, x GlyphIndex) (string, error) { } } +// GlyphAdvance returns the advance width for the x'th glyph. ppem is the +// number of pixels in 1 em. +// +// It returns ErrNotFound if the glyph index is out of range. +func (f *Font) GlyphAdvance(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, h font.Hinting) (fixed.Int26_6, error) { + if int(x) >= f.NumGlyphs() { + return 0, ErrNotFound + } + if b == nil { + b = &Buffer{} + } + + // https://www.microsoft.com/typography/OTSPEC/hmtx.htm says that "As an + // optimization, the number of records can be less than the number of + // glyphs, in which case the advance width value of the last record applies + // to all remaining glyph IDs." + if n := GlyphIndex(f.cached.numHMetrics - 1); x > n { + x = n + } + + buf, err := b.view(&f.src, int(f.hmtx.offset)+int(4*x), 2) + if err != nil { + return 0, err + } + adv := fixed.Int26_6(u16(buf)) + adv = scale(adv*ppem, f.cached.unitsPerEm) + if h == font.HintingFull { + // Quantize the fixed.Int26_6 value to the nearest pixel. + adv = (adv + 32) &^ 63 + } + return adv, nil +} + // Kern returns the horizontal adjustment for the kerning pair (x0, x1). A // positive kern means to move the glyphs further apart. ppem is the number of // pixels in 1 em. diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go index 2bb508b..3f4fcd5 100644 --- a/font/sfnt/sfnt_test.go +++ b/font/sfnt/sfnt_test.go @@ -11,6 +11,9 @@ import ( "path/filepath" "testing" + "golang.org/x/image/font" + "golang.org/x/image/font/gofont/gobold" + "golang.org/x/image/font/gofont/gomono" "golang.org/x/image/font/gofont/goregular" "golang.org/x/image/math/fixed" ) @@ -109,6 +112,74 @@ func testTrueType(t *testing.T, f *Font) { } } +func TestGlyphAdvance(t *testing.T) { + testCases := map[string][]struct { + r rune + want fixed.Int26_6 + }{ + "gobold": { + {' ', 569}, + {'A', 1479}, + {'Á', 1479}, + {'Æ', 2048}, + {'i', 592}, + {'x', 1139}, + }, + "gomono": { + {' ', 1229}, + {'A', 1229}, + {'Á', 1229}, + {'Æ', 1229}, + {'i', 1229}, + {'x', 1229}, + }, + "goregular": { + {' ', 569}, + {'A', 1366}, + {'Á', 1366}, + {'Æ', 2048}, + {'i', 505}, + {'x', 1024}, + }, + } + + var b Buffer + for name, testCases1 := range testCases { + data := []byte(nil) + switch name { + case "gobold": + data = gobold.TTF + case "gomono": + data = gomono.TTF + case "goregular": + data = goregular.TTF + } + f, err := Parse(data) + if err != nil { + t.Errorf("Parse(%q): %v", name, err) + continue + } + ppem := fixed.Int26_6(f.UnitsPerEm()) + + for _, tc := range testCases1 { + x, err := f.GlyphIndex(&b, tc.r) + if err != nil { + t.Errorf("name=%q, r=%q: GlyphIndex: %v", name, tc.r, err) + continue + } + got, err := f.GlyphAdvance(&b, x, ppem, font.HintingNone) + if err != nil { + t.Errorf("name=%q, r=%q: GlyphAdvance: %v", name, tc.r, err) + continue + } + if got != tc.want { + t.Errorf("name=%q, r=%q: GlyphAdvance: got %d, want %d", name, tc.r, got, tc.want) + continue + } + } + } +} + func TestGoRegularGlyphIndex(t *testing.T) { f, err := Parse(goregular.TTF) if err != nil { diff --git a/font/sfnt/truetype.go b/font/sfnt/truetype.go index 25455e5..ab27f5b 100644 --- a/font/sfnt/truetype.go +++ b/font/sfnt/truetype.go @@ -51,7 +51,7 @@ func midPoint(p, q fixed.Point26_6) fixed.Point26_6 { } } -func parseLoca(src *source, loca table, glyfOffset uint32, indexToLocFormat bool, numGlyphs int) (locations []uint32, err error) { +func parseLoca(src *source, loca table, glyfOffset uint32, indexToLocFormat bool, numGlyphs int32) (locations []uint32, err error) { if indexToLocFormat { if loca.length != 4*uint32(numGlyphs+1) { return nil, errInvalidLocaTable