font/sfnt: implement Font.GlyphBounds
Updates golang/go#30699
Font.GlyphBounds returns the glyph's bounding box and advance as
expected by the GlyphBounds method of the font.Face interface.
Change-Id: Iaee8b6d88afc48f21d00bf84219b99f993b3ab9a
Reviewed-on: https://go-review.googlesource.com/c/image/+/166477
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/font/font.go b/font/font.go
index 4d9d63c..d1a7535 100644
--- a/font/font.go
+++ b/font/font.go
@@ -51,9 +51,11 @@
//
// It returns !ok if the face does not contain a glyph for r.
//
- // The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y. A
- // visual depiction of what these metrics are is at
- // https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
+ // The glyph's ascent and descent are equal to -bounds.Min.Y and
+ // +bounds.Max.Y. The glyph's left-side and right-side bearings are equal
+ // to bounds.Min.X and advance-bounds.Max.X. A visual depiction of what
+ // these metrics are is at
+ // https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyphterms_2x.png
GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool)
// GlyphAdvance returns the advance width of r's glyph.
diff --git a/font/sfnt/proprietary_test.go b/font/sfnt/proprietary_test.go
index 39c2cdd..66f7d4b 100644
--- a/font/sfnt/proprietary_test.go
+++ b/font/sfnt/proprietary_test.go
@@ -353,6 +353,26 @@
}
}
+ for r, tc := range proprietaryGlyphBoundsTestCases[qualifiedFilename] {
+ ppem := fixed.Int26_6(f.UnitsPerEm())
+ x, err := f.GlyphIndex(&buf, r)
+ if err != nil {
+ t.Errorf("GlyphIndex(%q): %v", r, err)
+ continue
+ }
+ gotBounds, gotAdv, err := f.GlyphBounds(&buf, x, ppem, font.HintingNone)
+ if err != nil {
+ t.Errorf("GlyphBounds(%q): %v", r, err)
+ continue
+ }
+ if gotBounds != tc.wantBounds {
+ t.Errorf("GlyphBounds(%q): got %#v, want %#v", r, gotBounds, tc.wantBounds)
+ }
+ if gotAdv != tc.wantAdv {
+ t.Errorf("GlyphBounds(%q): got %#v, want %#v", r, gotAdv, tc.wantAdv)
+ }
+ }
+
kernLoop:
for _, tc := range proprietaryKernTestCases[qualifiedFilename] {
var indexes [2]GlyphIndex
@@ -1263,6 +1283,79 @@
},
}
+type boundsTestCase struct {
+ wantBounds fixed.Rectangle26_6
+ wantAdv fixed.Int26_6
+}
+
+// proprietaryGlyphBoundsTestCases hold expected GlyphBounds. The
+// numerical values can be verified by running the ttx tool.
+// - Advance from hmtx width
+// - Bounds from TTGlyph (with flipped Y axis)
+var proprietaryGlyphBoundsTestCases = map[string]map[rune]boundsTestCase{
+ "adobe/SourceHanSansSC-Regular.otf": {
+ '!': {
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 95, Y: -749},
+ Max: fixed.Point26_6{X: 227, Y: 13},
+ },
+ wantAdv: 323,
+ },
+ },
+ "apple/Helvetica.dfont?0": {
+ 'i': {
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 132, Y: -1469},
+ Max: fixed.Point26_6{X: 315, Y: 0},
+ },
+ wantAdv: 455,
+ },
+ },
+ "microsoft/Arial.ttf": {
+ 'A': {
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: -3, Y: -1466},
+ Max: fixed.Point26_6{X: 1369, Y: 0},
+ },
+ wantAdv: 1366,
+ },
+ // U+01FA LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE is a
+ // compound glyph whose elements are also compound glyphs.
+ 'Ǻ': {
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: -3, Y: -2124},
+ Max: fixed.Point26_6{X: 1369, Y: 0},
+ },
+ wantAdv: 1366,
+ },
+ // U+FD3E ORNATE LEFT PARENTHESIS.
+ '﴾': {
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 127, Y: -1608},
+ Max: fixed.Point26_6{X: 560, Y: 429},
+ },
+ wantAdv: 653,
+ },
+ // U+FD3F ORNATE RIGHT PARENTHESIS is a transformed version of left parenthesis
+ '﴿': {
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 93, Y: -1608},
+ Max: fixed.Point26_6{X: 526, Y: 429},
+ },
+ wantAdv: 653,
+ },
+ },
+ "noto/NotoSans-Regular.ttf": {
+ 'i': {
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 160, Y: -1509},
+ Max: fixed.Point26_6{X: 371, Y: 0},
+ },
+ wantAdv: 528,
+ },
+ },
+}
+
type kernTestCase struct {
ppem fixed.Int26_6
hinting font.Hinting
diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go
index ec1ba67..b6045e7 100644
--- a/font/sfnt/sfnt.go
+++ b/font/sfnt/sfnt.go
@@ -1496,6 +1496,92 @@
}
}
+// GlyphBounds returns the bounding box of the x'th glyph, drawn at a dot equal
+// to the origin, and that glyph's advance width. ppem is the number of pixels
+// in 1 em.
+//
+// It returns ErrNotFound if the glyph index is out of range.
+//
+// The glyph's ascent and descent are equal to -bounds.Min.Y and +bounds.Max.Y.
+// The glyph's left-side and right-side bearings are equal to bounds.Min.X and
+// advance-bounds.Max.X. A visual depiction of what these metrics are is at
+// https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyphterms_2x.png
+func (f *Font) GlyphBounds(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, h font.Hinting) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, err error) {
+ if int(x) >= f.NumGlyphs() {
+ return fixed.Rectangle26_6{}, 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 fixed.Rectangle26_6{}, 0, err
+ }
+ advance = fixed.Int26_6(u16(buf))
+ advance = scale(advance*ppem, f.cached.unitsPerEm)
+
+ // Ignore the hmtx LSB entries and the glyf bounding boxes. Instead, always
+ // calculate bounds from the segments. OpenType does contain the bounds for
+ // each glyph in the glyf table, but the bounds are not available for
+ // compound glyphs. CFF/PostScript also have no explicit bounds and must be
+ // obtained from the segments.
+
+ seg, err := f.LoadGlyph(b, x, ppem, &LoadGlyphOptions{
+ // TODO: pass h, the font.Hinting.
+ })
+ if err != nil {
+ return fixed.Rectangle26_6{}, 0, err
+ }
+
+ if len(seg) > 0 {
+ bounds.Min.X = fixed.Int26_6(+(1 << 31) - 1)
+ bounds.Min.Y = fixed.Int26_6(+(1 << 31) - 1)
+ bounds.Max.X = fixed.Int26_6(-(1 << 31) + 0)
+ bounds.Max.Y = fixed.Int26_6(-(1 << 31) + 0)
+ for _, s := range seg {
+ n := 1
+ switch s.Op {
+ case SegmentOpQuadTo:
+ n = 2
+ case SegmentOpCubeTo:
+ n = 3
+ }
+ for i := 0; i < n; i++ {
+ if bounds.Max.X < s.Args[i].X {
+ bounds.Max.X = s.Args[i].X
+ }
+ if bounds.Min.X > s.Args[i].X {
+ bounds.Min.X = s.Args[i].X
+ }
+ if bounds.Max.Y < s.Args[i].Y {
+ bounds.Max.Y = s.Args[i].Y
+ }
+ if bounds.Min.Y > s.Args[i].Y {
+ bounds.Min.Y = s.Args[i].Y
+ }
+ }
+ }
+ }
+
+ if h == font.HintingFull {
+ // Quantize the fixed.Int26_6 value to the nearest pixel.
+ advance = (advance + 32) &^ 63
+ // TODO: hinting of bounds should be handled by LoadGlyph. See TODO
+ // above.
+ }
+
+ return bounds, advance, nil
+}
+
// GlyphAdvance returns the advance width for the x'th glyph. ppem is the
// number of pixels in 1 em.
//
diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go
index 60b45ea..315ab16 100644
--- a/font/sfnt/sfnt_test.go
+++ b/font/sfnt/sfnt_test.go
@@ -251,6 +251,89 @@
}
}
+func TestGlyphBounds(t *testing.T) {
+ f, err := Parse(goregular.TTF)
+ if err != nil {
+ t.Fatalf("Parse: %v", err)
+ }
+ ppem := fixed.Int26_6(f.UnitsPerEm())
+
+ testCases := []struct {
+ r rune
+ wantBounds fixed.Rectangle26_6
+ wantAdv fixed.Int26_6
+ }{{
+ r: ' ',
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 0, Y: 0},
+ Max: fixed.Point26_6{X: 0, Y: 0},
+ },
+ wantAdv: 569,
+ }, {
+ r: 'A',
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 19, Y: -1480},
+ Max: fixed.Point26_6{X: 1342, Y: 0},
+ },
+ wantAdv: 1366,
+ }, {
+ r: 'Á',
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 19, Y: -1935},
+ Max: fixed.Point26_6{X: 1342, Y: 0},
+ },
+ wantAdv: 1366,
+ }, {
+ r: 'Æ',
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 19, Y: -1480},
+ Max: fixed.Point26_6{X: 1990, Y: 0},
+ },
+ wantAdv: 2048,
+ }, {
+ r: 'i',
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 144, Y: -1500},
+ Max: fixed.Point26_6{X: 361, Y: 0}},
+ wantAdv: 505,
+ }, {
+ r: 'j',
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: -84, Y: -1500},
+ Max: fixed.Point26_6{X: 387, Y: 419},
+ },
+ wantAdv: 519,
+ }, {
+ r: 'x',
+ wantBounds: fixed.Rectangle26_6{
+ Min: fixed.Point26_6{X: 28, Y: -1086},
+ Max: fixed.Point26_6{X: 993, Y: 0},
+ },
+ wantAdv: 1024,
+ }}
+
+ var b Buffer
+ for _, tc := range testCases {
+ gi, err := f.GlyphIndex(&b, tc.r)
+ if err != nil {
+ t.Errorf("r=%q: %v", tc.r, err)
+ continue
+ }
+
+ gotBounds, gotAdv, err := f.GlyphBounds(&b, gi, ppem, font.HintingNone)
+ if err != nil {
+ t.Errorf("r=%q: GlyphBounds: %v", tc.r, err)
+ continue
+ }
+ if gotBounds != tc.wantBounds {
+ t.Errorf("r=%q: Bounds: got %#v, want %#v", tc.r, gotBounds, tc.wantBounds)
+ }
+ if gotAdv != tc.wantAdv {
+ t.Errorf("r=%q: Adv: got %#v, want %#v", tc.r, gotAdv, tc.wantAdv)
+ }
+ }
+}
+
func TestGlyphAdvance(t *testing.T) {
testCases := map[string][]struct {
r rune