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