From e1a1ede6891c997b81b830dbc6f4d2850b9392a4 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Sat, 15 Sep 2018 11:34:11 +0200 Subject: [PATCH] font,font/sfnt: expose font x-Height and capHeight Change-Id: I6e3e6e51c7e270e16413c23990f6df5e22cbfeb6 Reviewed-on: https://go-review.googlesource.com/135555 Run-TryBot: Elias Naur TryBot-Result: Gobot Gobot Reviewed-by: Nigel Tao --- font/basicfont/basicfont.go | 8 ++++--- font/basicfont/basicfont_test.go | 18 ++++++++++++++++ font/font.go | 8 +++++++ font/opentype/face_test.go | 2 +- font/plan9font/plan9font.go | 22 +++++++++++++------ font/plan9font/plan9font_test.go | 32 +++++++++++++++++++++++++++ font/sfnt/sfnt.go | 37 +++++++++++++++++++++++++++++--- font/sfnt/sfnt_test.go | 4 ++-- 8 files changed, 116 insertions(+), 15 deletions(-) create mode 100644 font/basicfont/basicfont_test.go diff --git a/font/basicfont/basicfont.go b/font/basicfont/basicfont.go index 1acc79f..acd1179 100644 --- a/font/basicfont/basicfont.go +++ b/font/basicfont/basicfont.go @@ -77,9 +77,11 @@ func (f *Face) Kern(r0, r1 rune) fixed.Int26_6 { return 0 } func (f *Face) Metrics() font.Metrics { return font.Metrics{ - Height: fixed.I(f.Height), - Ascent: fixed.I(f.Ascent), - Descent: fixed.I(f.Descent), + Height: fixed.I(f.Height), + Ascent: fixed.I(f.Ascent), + Descent: fixed.I(f.Descent), + XHeight: fixed.I(f.Ascent), + CapHeight: fixed.I(f.Ascent), } } diff --git a/font/basicfont/basicfont_test.go b/font/basicfont/basicfont_test.go new file mode 100644 index 0000000..f3409d7 --- /dev/null +++ b/font/basicfont/basicfont_test.go @@ -0,0 +1,18 @@ +// Copyright 2018 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 basicfont + +import ( + "testing" + + "golang.org/x/image/font" +) + +func TestMetrics(t *testing.T) { + want := font.Metrics{Height: 832, Ascent: 704, Descent: 128, XHeight: 704, CapHeight: 704} + if got := Face7x13.Metrics(); got != want { + t.Errorf("Face7x13: Metrics: got %v want %v", got, want) + } +} diff --git a/font/font.go b/font/font.go index 05f4357..56a3310 100644 --- a/font/font.go +++ b/font/font.go @@ -86,6 +86,14 @@ type Metrics struct { // value is typically positive, even though a descender goes below the // baseline. Descent fixed.Int26_6 + + // XHeight is the distance from the top of non-ascending lowercase letters + // to the baseline. + XHeight fixed.Int26_6 + + // CapHeight is the distance from the top of uppercase letters to the + // baseline. + CapHeight fixed.Int26_6 } // Drawer draws text on a destination image. diff --git a/font/opentype/face_test.go b/font/opentype/face_test.go index 524a7f6..8389bd8 100644 --- a/font/opentype/face_test.go +++ b/font/opentype/face_test.go @@ -82,7 +82,7 @@ func TestFaceKern(t *testing.T) { } func TestFaceMetrics(t *testing.T) { - want := font.Metrics{Height: 888, Ascent: 726, Descent: 162} + want := font.Metrics{Height: 888, Ascent: 726, Descent: 162, XHeight: 407, CapHeight: 555} got := regular.Metrics() if got != want { t.Fatalf("metrics failed. got=%#v. want=%#v", got, want) diff --git a/font/plan9font/plan9font.go b/font/plan9font/plan9font.go index 315e793..82cd882 100644 --- a/font/plan9font/plan9font.go +++ b/font/plan9font/plan9font.go @@ -62,10 +62,16 @@ func (f *subface) Close() error { return nil } func (f *subface) Kern(r0, r1 rune) fixed.Int26_6 { return 0 } func (f *subface) Metrics() font.Metrics { + // Approximate XHeight with the ascent of lowercase 'x'. + xbounds, _, _ := f.GlyphBounds('x') + // The same applies to CapHeight, using the uppercase 'H'. + hbounds, _, _ := f.GlyphBounds('H') return font.Metrics{ - Height: fixed.I(f.height), - Ascent: fixed.I(f.ascent), - Descent: fixed.I(f.height - f.ascent), + Height: fixed.I(f.height), + Ascent: fixed.I(f.ascent), + Descent: fixed.I(f.height - f.ascent), + XHeight: -xbounds.Min.Y, + CapHeight: -hbounds.Min.Y, } } @@ -144,10 +150,14 @@ func (f *face) Close() error { return nil } func (f *face) Kern(r0, r1 rune) fixed.Int26_6 { return 0 } func (f *face) Metrics() font.Metrics { + xbounds, _, _ := f.GlyphBounds('x') + hbounds, _, _ := f.GlyphBounds('H') return font.Metrics{ - Height: fixed.I(f.height), - Ascent: fixed.I(f.ascent), - Descent: fixed.I(f.height - f.ascent), + Height: fixed.I(f.height), + Ascent: fixed.I(f.ascent), + Descent: fixed.I(f.height - f.ascent), + XHeight: -xbounds.Min.Y, + CapHeight: -hbounds.Min.Y, } } diff --git a/font/plan9font/plan9font_test.go b/font/plan9font/plan9font_test.go index 23393a1..04a701d 100644 --- a/font/plan9font/plan9font_test.go +++ b/font/plan9font/plan9font_test.go @@ -6,10 +6,42 @@ package plan9font import ( "io/ioutil" + "path" "path/filepath" "testing" + + "golang.org/x/image/font" ) +func TestMetrics(t *testing.T) { + readFile := func(name string) ([]byte, error) { + return ioutil.ReadFile(filepath.FromSlash(path.Join("../testdata/fixed", name))) + } + data, err := readFile("unicode.7x13.font") + if err != nil { + t.Fatal(err) + } + face, err := ParseFont(data, readFile) + if err != nil { + t.Fatal(err) + } + want := font.Metrics{Height: 832, Ascent: 704, Descent: 128, XHeight: 704, CapHeight: 704} + if got := face.Metrics(); got != want { + t.Errorf("unicode.7x13.font: Metrics: got %v, want %v", got, want) + } + subData, err := readFile("7x13.0000") + if err != nil { + t.Fatal(err) + } + subFace, err := ParseSubfont(subData, 0) + if err != nil { + t.Fatal(err) + } + if got := subFace.Metrics(); got != want { + t.Errorf("7x13.0000: Metrics: got %v, want %v", got, want) + } +} + func BenchmarkParseSubfont(b *testing.B) { subfontData, err := ioutil.ReadFile(filepath.FromSlash("../testdata/fixed/7x13.0000")) if err != nil { diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go index 21a7927..3f952e9 100644 --- a/font/sfnt/sfnt.go +++ b/font/sfnt/sfnt.go @@ -86,6 +86,7 @@ var ( errInvalidLocationData = errors.New("sfnt: invalid location data") errInvalidMaxpTable = errors.New("sfnt: invalid maxp table") errInvalidNameTable = errors.New("sfnt: invalid name table") + errInvalidOS2Table = errors.New("sfnt: invalid OS/2 table") errInvalidPostTable = errors.New("sfnt: invalid post table") errInvalidSingleFont = errors.New("sfnt: invalid single font (data is a font collection)") errInvalidSourceData = errors.New("sfnt: invalid source data") @@ -565,6 +566,7 @@ type Font struct { cached struct { ascent int32 + capHeight int32 glyphData glyphData glyphIndex glyphIndexFunc bounds [4]int16 @@ -578,6 +580,7 @@ type Font struct { numHMetrics int32 postTableVersion uint32 unitsPerEm Units + xHeight int32 } } @@ -632,12 +635,17 @@ func (f *Font) initialize(offset int, isDfont bool) error { if err != nil { return err } + buf, xHeight, capHeight, err := f.parseOS2(buf) + if err != nil { + return err + } buf, postTableVersion, err := f.parsePost(buf, numGlyphs) if err != nil { return err } f.cached.ascent = ascent + f.cached.capHeight = capHeight f.cached.glyphData = glyphData f.cached.glyphIndex = glyphIndex f.cached.bounds = bounds @@ -651,6 +659,7 @@ func (f *Font) initialize(offset int, isDfont bool) error { f.cached.numHMetrics = numHMetrics f.cached.postTableVersion = postTableVersion f.cached.unitsPerEm = unitsPerEm + f.cached.xHeight = xHeight return nil } @@ -1045,6 +1054,24 @@ func (f *Font) parseGlyphData(buf []byte, numGlyphs int32, indexToLocFormat, isP return buf, ret, isColorBitmap, nil } +func (f *Font) parseOS2(buf []byte) (buf1 []byte, xHeight, capHeight int32, err error) { + // https://docs.microsoft.com/da-dk/typography/opentype/spec/os2 + + const headerSize = 96 + if f.os2.length < headerSize { + return nil, 0, 0, errInvalidOS2Table + } + xh, err := f.src.u16(buf, f.os2, 86) + if err != nil { + return nil, 0, 0, err + } + ch, err := f.src.u16(buf, f.os2, 88) + if err != nil { + return nil, 0, 0, err + } + return buf, int32(int16(xh)), int32(int16(ch)), nil +} + func (f *Font) parsePost(buf []byte, numGlyphs int32) (buf1 []byte, postTableVersion uint32, err error) { // https://www.microsoft.com/typography/otspec/post.htm @@ -1357,15 +1384,19 @@ func (f *Font) Kern(b *Buffer, x0, x1 GlyphIndex, ppem fixed.Int26_6, h font.Hin // Metrics returns the metrics of this font. func (f *Font) Metrics(b *Buffer, ppem fixed.Int26_6, h font.Hinting) (font.Metrics, error) { m := font.Metrics{ - Height: scale(fixed.Int26_6(f.cached.ascent-f.cached.descent+f.cached.lineGap)*ppem, f.cached.unitsPerEm), - Ascent: +scale(fixed.Int26_6(f.cached.ascent)*ppem, f.cached.unitsPerEm), - Descent: -scale(fixed.Int26_6(f.cached.descent)*ppem, f.cached.unitsPerEm), + Height: scale(fixed.Int26_6(f.cached.ascent-f.cached.descent+f.cached.lineGap)*ppem, f.cached.unitsPerEm), + Ascent: +scale(fixed.Int26_6(f.cached.ascent)*ppem, f.cached.unitsPerEm), + Descent: -scale(fixed.Int26_6(f.cached.descent)*ppem, f.cached.unitsPerEm), + XHeight: scale(fixed.Int26_6(f.cached.xHeight)*ppem, f.cached.unitsPerEm), + CapHeight: scale(fixed.Int26_6(f.cached.capHeight)*ppem, f.cached.unitsPerEm), } if h == font.HintingFull { // Quantize up to a whole pixel. m.Height = (m.Height + 63) &^ 63 m.Ascent = (m.Ascent + 63) &^ 63 m.Descent = (m.Descent + 63) &^ 63 + m.XHeight = (m.XHeight + 63) &^ 63 + m.CapHeight = (m.CapHeight + 63) &^ 63 } return m, nil } diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go index ca52fba..f5ddd2b 100644 --- a/font/sfnt/sfnt_test.go +++ b/font/sfnt/sfnt_test.go @@ -223,9 +223,9 @@ func TestMetrics(t *testing.T) { font []byte want font.Metrics }{ - "goregular": {goregular.TTF, font.Metrics{Height: 2367, Ascent: 1935, Descent: 432}}, + "goregular": {goregular.TTF, font.Metrics{Height: 2367, Ascent: 1935, Descent: 432, XHeight: 1086, CapHeight: 1480}}, // cmapTest.ttf has a non-zero lineGap. - "cmapTest": {cmapFont, font.Metrics{Height: 1549, Ascent: 1365, Descent: 0}}, + "cmapTest": {cmapFont, font.Metrics{Height: 1549, Ascent: 1365, Descent: 0, XHeight: 800, CapHeight: 800}}, } var b Buffer for name, tc := range testCases {