font/sfnt: support for kerning from GPOS tables

This CL adds support for reading kerning information from GPOS tables.

Most modern fonts like Noto or Roboto do not provide a kern table.
Instead they use the GPOS table for kerning.

This CL is limited to horizontal kerning and latin scripts.

The proprietary_test was extended to check all supported variations.
gpos.go has full test coverage (excluding error conditions).
Additionally, the new TestBulkKern (opt-in) can be used to check parsing
of large font collection.

Fixes: golang/go#29528

Change-Id: I9f1ae142c9e26170dd836ccaca1526bbefe9698a
Reviewed-on: https://go-review.googlesource.com/c/159638
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/font/sfnt/gpos.go b/font/sfnt/gpos.go
new file mode 100644
index 0000000..f136877
--- /dev/null
+++ b/font/sfnt/gpos.go
@@ -0,0 +1,544 @@
+// Copyright 2019 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 sfnt
+
+import (
+	"sort"
+)
+
+const (
+	hexScriptLatn  = uint32(0x6c61746e) // latn
+	hexScriptDFLT  = uint32(0x44464c54) // DFLT
+	hexFeatureKern = uint32(0x6b65726e) // kern
+)
+
+//kernFunc returns the unscaled kerning value for kerning pair a+b.
+// Returns ErrNotFound if no kerning is specified for this pair.
+type kernFunc func(a, b GlyphIndex) (int16, error)
+
+func (f *Font) parseGPOSKern(buf []byte) ([]byte, []kernFunc, error) {
+	// https://docs.microsoft.com/en-us/typography/opentype/spec/gpos
+
+	if f.gpos.length == 0 {
+		return buf, nil, nil
+	}
+	const headerSize = 10 // GPOS header v1.1 is 14 bytes, but we don't support FeatureVariations
+	if f.gpos.length < headerSize {
+		return buf, nil, errInvalidGPOSTable
+	}
+
+	buf, err := f.src.view(buf, int(f.gpos.offset), headerSize)
+	if err != nil {
+		return buf, nil, err
+	}
+
+	// check for version 1.0/1.1
+	if u16(buf) != 1 || u16(buf[2:]) > 1 {
+		return buf, nil, errUnsupportedGPOSTable
+	}
+	scriptListOffset := u16(buf[4:])
+	featureListOffset := u16(buf[6:])
+	lookupListOffset := u16(buf[8:])
+
+	// get all feature indices for latn script
+	buf, featureIdxs, err := f.parseGPOSScriptFeatures(buf, int(f.gpos.offset)+int(scriptListOffset), hexScriptLatn)
+	if err != nil {
+		return buf, nil, err
+	}
+	if len(featureIdxs) == 0 {
+		// get all feature indices for DFLT script
+		buf, featureIdxs, err = f.parseGPOSScriptFeatures(buf, int(f.gpos.offset)+int(scriptListOffset), hexScriptDFLT)
+		if err != nil {
+			return buf, nil, err
+		}
+		if len(featureIdxs) == 0 {
+			return buf, nil, nil
+		}
+	}
+
+	// get all lookup indices for kern features
+	buf, lookupIdx, err := f.parseGPOSFeaturesLookup(buf, int(f.gpos.offset)+int(featureListOffset), featureIdxs, hexFeatureKern)
+
+	// LookupTableList: lookupCount,[]lookups
+	buf, numLookupTables, err := f.src.varLenView(buf, int(f.gpos.offset)+int(lookupListOffset), 2, 0, 2)
+	if err != nil {
+		return buf, nil, err
+	}
+
+	var kernFuncs []kernFunc
+
+lookupTables:
+	for _, n := range lookupIdx {
+		if n > numLookupTables {
+			return buf, nil, errInvalidGPOSTable
+		}
+		tableOffset := int(f.gpos.offset) + int(lookupListOffset) + int(u16(buf[2+n*2:]))
+
+		// LookupTable: lookupType, lookupFlag, subTableCount, []subtableOffsets, markFilteringSet
+		buf, numSubTables, err := f.src.varLenView(buf, tableOffset, 8, 4, 2)
+		if err != nil {
+			return buf, nil, err
+		}
+
+		flags := u16(buf[2:])
+
+		subTableOffsets := make([]int, numSubTables)
+		for i := 0; i < int(numSubTables); i++ {
+			subTableOffsets[i] = int(tableOffset) + int(u16(buf[6+i*2:]))
+		}
+
+		switch lookupType := u16(buf); lookupType {
+		case 2: // PairPos table
+		case 9:
+			// Extension Positioning table defines an additional u32 offset
+			// to allow subtables to exceed the 16-bit limit.
+			for i := range subTableOffsets {
+				buf, err = f.src.view(buf, subTableOffsets[i], 8)
+				if err != nil {
+					return buf, nil, err
+				}
+				if format := u16(buf); format != 1 {
+					return buf, nil, errUnsupportedExtensionPosFormat
+				}
+				if lookupType := u16(buf[2:]); lookupType != 2 {
+					continue lookupTables
+				}
+				subTableOffsets[i] += int(u32(buf[4:]))
+			}
+		default: // other types are not supported
+			continue
+		}
+
+		if flags&0x0010 > 0 {
+			// useMarkFilteringSet enabled, skip as it is not supported
+			continue
+		}
+
+		for _, subTableOffset := range subTableOffsets {
+			buf, err = f.src.view(buf, int(subTableOffset), 4)
+			if err != nil {
+				return buf, nil, err
+			}
+			format := u16(buf)
+
+			var lookupIndex indexLookupFunc
+			buf, lookupIndex, err = f.makeCachedCoverageLookup(buf, subTableOffset+int(u16(buf[2:])))
+			if err != nil {
+				return buf, nil, err
+			}
+
+			switch format {
+			case 1: // Adjustments for Glyph Pairs
+				buf, kern, err := f.parsePairPosFormat1(buf, subTableOffset, lookupIndex)
+				if err != nil {
+					return buf, nil, err
+				}
+				if kern != nil {
+					kernFuncs = append(kernFuncs, kern)
+				}
+			case 2: // Class Pair Adjustment
+				buf, kern, err := f.parsePairPosFormat2(buf, subTableOffset, lookupIndex)
+				if err != nil {
+					return buf, nil, err
+				}
+				if kern != nil {
+					kernFuncs = append(kernFuncs, kern)
+				}
+			}
+		}
+	}
+
+	return buf, kernFuncs, nil
+}
+
+func (f *Font) parsePairPosFormat1(buf []byte, offset int, lookupIndex indexLookupFunc) ([]byte, kernFunc, error) {
+	// PairPos Format 1: posFormat, coverageOffset, valueFormat1,
+	// valueFormat2, pairSetCount, []pairSetOffsets
+	var err error
+	var nPairs int
+	buf, nPairs, err = f.src.varLenView(buf, offset, 10, 8, 2)
+	if err != nil {
+		return buf, nil, err
+	}
+	// check valueFormat1 and valueFormat2 flags
+	if u16(buf[4:]) != 0x04 || u16(buf[6:]) != 0x00 {
+		// we only support kerning with X_ADVANCE for first glyph
+		return buf, nil, nil
+	}
+
+	// PairPos table contains an array of offsets to PairSet
+	// tables, which contains an array of PairValueRecords.
+	// Calculate length of complete PairPos table by jumping to
+	// last PairSet.
+	// We need to iterate all offsets to find the last pair as
+	// offsets are not sorted and can be repeated.
+	var lastPairSetOffset int
+	for n := 0; n < nPairs; n++ {
+		pairOffset := int(u16(buf[10+n*2:]))
+		if pairOffset > lastPairSetOffset {
+			lastPairSetOffset = pairOffset
+		}
+	}
+	buf, err = f.src.view(buf, offset+lastPairSetOffset, 2)
+	if err != nil {
+		return buf, nil, err
+	}
+
+	pairValueCount := int(u16(buf))
+	// Each PairSet contains the secondGlyph (u16) and one or more value records (all u16).
+	// We only support lookup tables with one value record (X_ADVANCE, see valueFormat1/2 above).
+	lastPairSetLength := 2 + pairValueCount*4
+
+	length := lastPairSetOffset + lastPairSetLength
+	buf, err = f.src.view(buf, offset, length)
+	if err != nil {
+		return buf, nil, err
+	}
+
+	kern := makeCachedPairPosGlyph(lookupIndex, nPairs, buf)
+	return buf, kern, nil
+}
+
+func (f *Font) parsePairPosFormat2(buf []byte, offset int, lookupIndex indexLookupFunc) ([]byte, kernFunc, error) {
+	// PairPos Format 2:
+	// posFormat, coverageOffset, valueFormat1, valueFormat2,
+	// classDef1Offset, classDef2Offset, class1Count, class2Count,
+	// []class1Records
+	var err error
+	buf, err = f.src.view(buf, offset, 16)
+	if err != nil {
+		return buf, nil, err
+	}
+	// check valueFormat1 and valueFormat2 flags
+	if u16(buf[4:]) != 0x04 || u16(buf[6:]) != 0x00 {
+		// we only support kerning with X_ADVANCE for first glyph
+		return buf, nil, nil
+	}
+	numClass1 := int(u16(buf[12:]))
+	numClass2 := int(u16(buf[14:]))
+	cdef1Offset := offset + int(u16(buf[8:]))
+	cdef2Offset := offset + int(u16(buf[10:]))
+	var cdef1, cdef2 classLookupFunc
+	buf, cdef1, err = f.makeCachedClassLookup(buf, cdef1Offset)
+	if err != nil {
+		return buf, nil, err
+	}
+	buf, cdef2, err = f.makeCachedClassLookup(buf, cdef2Offset)
+	if err != nil {
+		return buf, nil, err
+	}
+
+	buf, err = f.src.view(buf, offset+16, numClass1*numClass2*2)
+	if err != nil {
+		return buf, nil, err
+	}
+	kern := makeCachedPairPosClass(
+		lookupIndex,
+		numClass1,
+		numClass2,
+		cdef1,
+		cdef2,
+		buf,
+	)
+
+	return buf, kern, nil
+}
+
+// parseGPOSScriptFeatures returns all indices of features in FeatureTable that
+// are valid for the given script.
+// Returns features from DefaultLangSys, different languages are not supported.
+// However, all observed fonts either do not use different languages or use the
+// same features as DefaultLangSys.
+func (f *Font) parseGPOSScriptFeatures(buf []byte, offset int, script uint32) ([]byte, []int, error) {
+	// ScriptList table: scriptCount, []scriptRecords{scriptTag, scriptOffset}
+	buf, numScriptTables, err := f.src.varLenView(buf, offset, 2, 0, 6)
+	if err != nil {
+		return buf, nil, err
+	}
+
+	// Search ScriptTables for script
+	var scriptTableOffset uint16
+	for i := 0; i < numScriptTables; i++ {
+		scriptTag := u32(buf[2+i*6:])
+		if scriptTag == script {
+			scriptTableOffset = u16(buf[2+i*6+4:])
+			break
+		}
+	}
+	if scriptTableOffset == 0 {
+		return buf, nil, nil
+	}
+
+	// Script table: defaultLangSys, langSysCount, []langSysRecords{langSysTag, langSysOffset}
+	buf, err = f.src.view(buf, offset+int(scriptTableOffset), 2)
+	if err != nil {
+		return buf, nil, err
+	}
+	defaultLangSysOffset := u16(buf)
+
+	if defaultLangSysOffset == 0 {
+		return buf, nil, nil
+	}
+
+	// LangSys table: lookupOrder (reserved), requiredFeatureIndex, featureIndexCount, []featureIndices
+	buf, numFeatures, err := f.src.varLenView(buf, offset+int(scriptTableOffset)+int(defaultLangSysOffset), 6, 4, 2)
+
+	featureIdxs := make([]int, numFeatures)
+	for i := range featureIdxs {
+		featureIdxs[i] = int(u16(buf[6+i*2:]))
+	}
+	return buf, featureIdxs, nil
+}
+
+func (f *Font) parseGPOSFeaturesLookup(buf []byte, offset int, featureIdxs []int, feature uint32) ([]byte, []int, error) {
+	// FeatureList table: featureCount, []featureRecords{featureTag, featureOffset}
+	buf, numFeatureTables, err := f.src.varLenView(buf, offset, 2, 0, 6)
+	if err != nil {
+		return buf, nil, err
+	}
+
+	lookupIdx := make([]int, 0, 4)
+
+	for _, fidx := range featureIdxs {
+		if fidx > numFeatureTables {
+			return buf, nil, errInvalidGPOSTable
+		}
+		featureTag := u32(buf[2+fidx*6:])
+		if featureTag != feature {
+			continue
+		}
+		featureOffset := u16(buf[2+fidx*6+4:])
+
+		buf, numLookups, err := f.src.varLenView(nil, offset+int(featureOffset), 4, 2, 2)
+		if err != nil {
+			return buf, nil, err
+		}
+
+		for i := 0; i < numLookups; i++ {
+			lookupIdx = append(lookupIdx, int(u16(buf[4+i*2:])))
+		}
+	}
+
+	return buf, lookupIdx, nil
+}
+
+func makeCachedPairPosGlyph(cov indexLookupFunc, num int, buf []byte) kernFunc {
+	glyphs := make([]byte, len(buf))
+	copy(glyphs, buf)
+	return func(a, b GlyphIndex) (int16, error) {
+		idx, found := cov(a)
+		if !found {
+			return 0, ErrNotFound
+		}
+		if idx >= num {
+			return 0, ErrNotFound
+		}
+		offset := int(u16(glyphs[10+idx*2:]))
+		if offset+1 >= len(glyphs) {
+			return 0, errInvalidGPOSTable
+		}
+
+		count := int(u16(glyphs[offset:]))
+		for i := 0; i < count; i++ {
+			secondGlyphIndex := GlyphIndex(int(u16(glyphs[offset+2+i*4:])))
+			if secondGlyphIndex == b {
+				return int16(u16(glyphs[offset+2+i*4+2:])), nil
+			}
+			if secondGlyphIndex > b {
+				return 0, ErrNotFound
+			}
+		}
+
+		return 0, ErrNotFound
+	}
+}
+
+func makeCachedPairPosClass(cov indexLookupFunc, num1, num2 int, cdef1, cdef2 classLookupFunc, buf []byte) kernFunc {
+	glyphs := make([]byte, len(buf))
+	copy(glyphs, buf)
+	return func(a, b GlyphIndex) (int16, error) {
+		// check coverage to avoid selection of default class 0
+		_, found := cov(a)
+		if !found {
+			return 0, ErrNotFound
+		}
+		idxa := cdef1(a)
+		idxb := cdef2(b)
+		return int16(u16(glyphs[(idxb+idxa*num2)*2:])), nil
+	}
+}
+
+// indexLookupFunc returns the index into a PairPos table for the provided glyph.
+// Returns false if the glyph is not covered by this lookup.
+type indexLookupFunc func(GlyphIndex) (int, bool)
+
+func (f *Font) makeCachedCoverageLookup(buf []byte, offset int) ([]byte, indexLookupFunc, error) {
+	var err error
+	buf, err = f.src.view(buf, offset, 2)
+	if err != nil {
+		return buf, nil, err
+	}
+	switch u16(buf) {
+	case 1:
+		// Coverage Format 1: coverageFormat, glyphCount, []glyphArray
+		buf, _, err = f.src.varLenView(buf, offset, 4, 2, 2)
+		if err != nil {
+			return buf, nil, err
+		}
+		return buf, makeCachedCoverageList(buf[2:]), nil
+	case 2:
+		// Coverage Format 2: coverageFormat, rangeCount, []rangeRecords{startGlyphID, endGlyphID, startCoverageIndex}
+		buf, _, err = f.src.varLenView(buf, offset, 4, 2, 6)
+		if err != nil {
+			return buf, nil, err
+		}
+		return buf, makeCachedCoverageRange(buf[2:]), nil
+	default:
+		return buf, nil, errUnsupportedCoverageFormat
+	}
+}
+
+func makeCachedCoverageList(buf []byte) indexLookupFunc {
+	num := int(u16(buf))
+	list := make([]byte, len(buf)-2)
+	copy(list, buf[2:])
+	return func(gi GlyphIndex) (int, bool) {
+		idx := sort.Search(num, func(i int) bool {
+			return gi <= GlyphIndex(u16(list[i*2:]))
+		})
+		if idx < num && GlyphIndex(u16(list[idx*2:])) == gi {
+			return idx, true
+		}
+
+		return 0, false
+	}
+}
+
+func makeCachedCoverageRange(buf []byte) indexLookupFunc {
+	num := int(u16(buf))
+	ranges := make([]byte, len(buf)-2)
+	copy(ranges, buf[2:])
+	return func(gi GlyphIndex) (int, bool) {
+		if num == 0 {
+			return 0, false
+		}
+
+		// ranges is an array of startGlyphID, endGlyphID and startCoverageIndex
+		// Ranges are non-overlapping.
+		// The following GlyphIDs/index pairs are stored as follows:
+		//	 pairs: 130=0, 131=1, 132=2, 133=3, 134=4, 135=5, 137=6
+		//   ranges: 130, 135, 0    137, 137, 6
+		// startCoverageIndex is used to calculate the index without counting
+		// the length of the preceeding ranges
+
+		idx := sort.Search(num, func(i int) bool {
+			return gi <= GlyphIndex(u16(ranges[i*6:]))
+		})
+		// idx either points to a matching start, or to the next range (or idx==num)
+		// e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range
+
+		// check if gi is the start of a range, but only if sort.Search returned a valid result
+		if idx < num {
+			if start := u16(ranges[idx*6:]); gi == GlyphIndex(start) {
+				return int(u16(ranges[idx*6+4:])), true
+			}
+		}
+		// check if gi is in previous range
+		if idx > 0 {
+			idx--
+			start, end := u16(ranges[idx*6:]), u16(ranges[idx*6+2:])
+			if gi >= GlyphIndex(start) && gi <= GlyphIndex(end) {
+				return int(u16(ranges[idx*6+4:]) + uint16(gi) - start), true
+			}
+		}
+
+		return 0, false
+	}
+}
+
+// classLookupFunc returns the class ID for the provided glyph. Returns 0
+// (default class) for glyphs not covered by this lookup.
+type classLookupFunc func(GlyphIndex) int
+
+func (f *Font) makeCachedClassLookup(buf []byte, offset int) ([]byte, classLookupFunc, error) {
+	var err error
+	buf, err = f.src.view(buf, offset, 2)
+	if err != nil {
+		return buf, nil, err
+	}
+	switch u16(buf) {
+	case 1:
+		// ClassDefFormat 1: classFormat, startGlyphID, glyphCount, []classValueArray
+		buf, _, err = f.src.varLenView(buf, offset, 6, 4, 2)
+		if err != nil {
+			return buf, nil, err
+		}
+		return buf, makeCachedClassLookupFormat1(buf), nil
+	case 2:
+		// ClassDefFormat 2: classFormat, classRangeCount, []classRangeRecords
+		buf, _, err = f.src.varLenView(buf, offset, 4, 2, 6)
+		if err != nil {
+			return buf, nil, err
+		}
+		return buf, makeCachedClassLookupFormat2(buf), nil
+	default:
+		return buf, nil, errUnsupportedClassDefFormat
+	}
+}
+
+func makeCachedClassLookupFormat1(buf []byte) classLookupFunc {
+	startGI := u16(buf[2:])
+	num := u16(buf[4:])
+	classIDs := make([]byte, len(buf)-4)
+	copy(classIDs, buf[6:])
+
+	return func(gi GlyphIndex) int {
+		// classIDs is an array of target class IDs. gi is the index into that array (minus startGI).
+		if gi < GlyphIndex(startGI) || gi >= GlyphIndex(startGI+num) {
+			// default to class 0
+			return 0
+		}
+		return int(u16(classIDs[(int(gi)-int(startGI))*2:]))
+	}
+}
+
+func makeCachedClassLookupFormat2(buf []byte) classLookupFunc {
+	num := int(u16(buf[2:]))
+	classRanges := make([]byte, len(buf)-2)
+	copy(classRanges, buf[4:])
+
+	return func(gi GlyphIndex) int {
+		if num == 0 {
+			return 0 // default to class 0
+		}
+
+		// classRange is an array of startGlyphID, endGlyphID and target class ID.
+		// Ranges are non-overlapping.
+		// E.g. 130, 135, 1   137, 137, 5   etc
+
+		idx := sort.Search(num, func(i int) bool {
+			return gi <= GlyphIndex(u16(classRanges[i*6:]))
+		})
+		// idx either points to a matching start, or to the next range (or idx==num)
+		// e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range
+
+		// check if gi is the start of a range, but only if sort.Search returned a valid result
+		if idx < num {
+			if start := u16(classRanges[idx*6:]); gi == GlyphIndex(start) {
+				return int(u16(classRanges[idx*6+4:]))
+			}
+		}
+		// check if gi is in previous range
+		if idx > 0 {
+			idx--
+			start, end := u16(classRanges[idx*6:]), u16(classRanges[idx*6+2:])
+			if gi >= GlyphIndex(start) && gi <= GlyphIndex(end) {
+				return int(u16(classRanges[idx*6+4:]))
+			}
+		}
+		// default to class 0
+		return 0
+	}
+}
diff --git a/font/sfnt/kern_test.go b/font/sfnt/kern_test.go
new file mode 100644
index 0000000..163dac7
--- /dev/null
+++ b/font/sfnt/kern_test.go
@@ -0,0 +1,141 @@
+// Copyright 2019 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 sfnt
+
+/*
+This file contains opt-in tests for kerning in user provided fonts.
+
+Kerning information in kern and GPOS tables can be quite complex. These tests
+recursively load all fonts from -bulkFontDirs and try to kern all possible
+glyph pairs.
+
+These tests only check if there are no errors during kerning. Tests of actual
+kerning values are in proprietary_test.go.
+
+Note: CJK fonts can contain billions of posible kerning pairs. Testing for
+these fonts stops after -bulkMaxKernPairs.
+
+To opt-in:
+
+go test golang.org/x/image/font/sfnt -test.run=BulkKern -args -bulk -bulkFontDirs /Library/Fonts:./myfonts
+*/
+
+import (
+	"flag"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"golang.org/x/image/font"
+	"golang.org/x/image/math/fixed"
+)
+
+var (
+	bulk = flag.Bool("bulk", false, "")
+
+	fontDirs = flag.String(
+		"bulkFontDirs",
+		"./",
+		"separated directories to search for fonts",
+	)
+	maxKernPairs = flag.Int(
+		"bulkMaxKernPairs",
+		20000000,
+		"skip testing of kerning after this many tested pairs",
+	)
+)
+
+func TestBulkKern(t *testing.T) {
+	if !*bulk {
+		t.Skip("skipping bulk font test")
+	}
+
+	for _, fontDir := range filepath.SplitList(*fontDirs) {
+		err := filepath.Walk(fontDir, func(path string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			if info.IsDir() {
+				return nil
+			}
+			if strings.HasSuffix(path, ".ttf") || strings.HasSuffix(path, ".otf") {
+				t.Run(info.Name(), testFontKerning(filepath.Join(path)))
+			}
+			return nil
+		})
+		if err != nil {
+			t.Fatal("error finding fonts", err)
+		}
+	}
+
+}
+
+func testFontKerning(fname string) func(*testing.T) {
+	return func(t *testing.T) {
+		t.Parallel()
+		b, err := ioutil.ReadFile(fname)
+		if err != nil {
+			t.Fatal(err)
+		}
+		fnt, err := Parse(b)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		buf := &Buffer{}
+
+		// collect all GlyphIndex
+		glyphs := make([]GlyphIndex, 1, fnt.NumGlyphs())
+		glyphs[0] = GlyphIndex(0)
+		r := rune(0)
+		for r < 0xffff {
+			g, err := fnt.GlyphIndex(buf, r)
+			r++
+			if g == 0 || err == ErrNotFound {
+				continue
+			}
+			if err != nil {
+				t.Fatal(err)
+			}
+			glyphs = append(glyphs, g)
+			if len(glyphs) == fnt.NumGlyphs() {
+				break
+			}
+		}
+
+		var kerned, tested int
+		for _, g1 := range glyphs {
+			for _, g2 := range glyphs {
+				if tested >= *maxKernPairs {
+					log.Printf("stop testing after %d or %d kerning pairs (found %d pairs)",
+						tested, len(glyphs)*len(glyphs), kerned)
+					return
+				}
+
+				tested++
+				adv, err := fnt.Kern(buf, g1, g2, fixed.I(20), font.HintingNone)
+				if err == ErrNotFound {
+					continue
+				}
+				if err != nil {
+					t.Fatal(err)
+				}
+				if adv != 0 {
+					kerned++
+				}
+			}
+		}
+
+		log.Printf("found %d kerning pairs for %d glyphs (%.1f%%) in %q",
+			kerned,
+			len(glyphs),
+			100*float64(kerned)/float64(len(glyphs)*len(glyphs)),
+			fname,
+		)
+	}
+}
diff --git a/font/sfnt/proprietary_test.go b/font/sfnt/proprietary_test.go
index bb14a34..39c2cdd 100644
--- a/font/sfnt/proprietary_test.go
+++ b/font/sfnt/proprietary_test.go
@@ -168,6 +168,10 @@
 	testProprietary(t, "apple", "ヒラギノ角ゴシック W0.ttc?1", 9000, -1)
 }
 
+func TestProprietaryDejaVuSans(t *testing.T) {
+	testProprietary(t, "dejavu", "DejaVuSans.ttf", 3300, -1)
+}
+
 func TestProprietaryDejaVuSansExtraLight(t *testing.T) {
 	testProprietary(t, "dejavu", "DejaVuSans-ExtraLight.ttf", 2000, -1)
 }
@@ -427,6 +431,7 @@
 	"apple/ヒラギノ角ゴシック W0.ttc?1": "11.0d7e1",
 
 	"dejavu/DejaVuSans-ExtraLight.ttf": "Version 2.37",
+	"dejavu/DejaVuSans.ttf":            "Version 2.37",
 	"dejavu/DejaVuSansMono.ttf":        "Version 2.37",
 	"dejavu/DejaVuSerif.ttf":           "Version 2.37",
 
@@ -464,6 +469,7 @@
 	"apple/ヒラギノ角ゴシック W0.ttc?1": ".Hiragino Kaku Gothic Interface W0",
 
 	"dejavu/DejaVuSans-ExtraLight.ttf": "DejaVu Sans ExtraLight",
+	"dejavu/DejaVuSans.ttf":            "DejaVu Sans",
 	"dejavu/DejaVuSansMono.ttf":        "DejaVu Sans Mono",
 	"dejavu/DejaVuSerif.ttf":           "DejaVu Serif",
 
@@ -1267,6 +1273,15 @@
 // proprietaryKernTestCases hold a sample of each font's kerning pairs. The
 // numerical values can be verified by running the ttx tool.
 var proprietaryKernTestCases = map[string][]kernTestCase{
+	"adobe/SourceSansPro-Regular.otf": {
+		{1000, font.HintingNone, [2]rune{'A', 'V'}, -14},
+		{1000, font.HintingNone, [2]rune{'F', '\u0129'}, 36},
+	},
+	"adobe/SourceHanSansSC-Regular.otf": {
+		// U+3043 HIRAGANA LETTER SMALL I
+		// U+3067 HIRAGANA LETTER DE
+		{1000, font.HintingNone, [2]rune{'\u3043', '\u3067'}, -20},
+	},
 	"dejavu/DejaVuSans-ExtraLight.ttf": {
 		{2048, font.HintingNone, [2]rune{'A', 'A'}, 57},
 		{2048, font.HintingNone, [2]rune{'W', 'A'}, -112},
@@ -1274,10 +1289,16 @@
 		// U+01FA LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE
 		// U+1E82 LATIN CAPITAL LETTER W WITH ACUTE
 		{2048, font.HintingNone, [2]rune{'\u00c1', 'A'}, 57},
-		// TODO: enable these next two test cases, when we support multiple
-		// kern subtables.
-		// {2048, font.HintingNone, [2]rune{'\u01fa', 'A'}, 57},
-		// {2048, font.HintingNone, [2]rune{'\u1e82', 'A'}, -112},
+		{2048, font.HintingNone, [2]rune{'\u01fa', 'A'}, 57},
+		{2048, font.HintingNone, [2]rune{'\u1e82', 'A'}, -112},
+	},
+	"dejavu/DejaVuSans.ttf": {
+		// U+EF13 + U+EF19 are private use codes, but DejaVuSans has these
+		// codes in a rarely used ClassDef format 1
+		{2048, font.HintingNone, [2]rune{'\uef13', '\uef19'}, -40},
+		{2048, font.HintingNone, [2]rune{'\uef13', '\uef13'}, 0},
+		// Use U+EF13 to trigger default class in ClassDef format 2
+		{2048, font.HintingNone, [2]rune{'A', '\uef13'}, 0},
 	},
 	"microsoft/Arial.ttf": {
 		{2048, font.HintingNone, [2]rune{'A', 'V'}, -152},
@@ -1309,6 +1330,35 @@
 	"microsoft/Webdings.ttf": {
 		{2048, font.HintingNone, [2]rune{'\uf041', '\uf042'}, 0},
 	},
+	"noto/NotoSans-Regular.ttf": {
+		{2048, font.HintingNone, [2]rune{'A', 'V'}, -40},
+		{2048, font.HintingNone, [2]rune{'A', 'W'}, -40},
+		{2048, font.HintingNone, [2]rune{'A', 'T'}, -70},
+		{2048, font.HintingNone, [2]rune{'V', 'A'}, -40},
+		{2048, font.HintingNone, [2]rune{'W', 'A'}, -40},
+		{2048, font.HintingNone, [2]rune{'T', 'A'}, -70},
+		// U+00C1 LATIN CAPITAL LETTER A WITH ACUTE
+		// U+01FA LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE
+		// U+1E82 LATIN CAPITAL LETTER W WITH ACUTE
+		{2048, font.HintingNone, [2]rune{'\u00c1', 'T'}, -70},
+		{2048, font.HintingNone, [2]rune{'\u01fa', 'T'}, -70},
+		{2048, font.HintingNone, [2]rune{'\u1e82', 'A'}, -40},
+
+		// NotoSans-Regular.ttf uses two PairPos format=1 tables (with ExtensionPos).
+		// Test first and last pairs in both tables.
+		// First pair in first table
+		{2048, font.HintingNone, [2]rune{'F', '?'}, 20},
+		// U+04c3 CYRILLIC CAPITAL LETTER KA WITH HOOK
+		// U+0424 CYRILLIC CAPITAL LETTER EF
+		// Last pair in first table
+		{2048, font.HintingNone, [2]rune{'\u04c3', '\u0424'}, -20},
+		// First pair in second table
+		{2048, font.HintingNone, [2]rune{'"', 'A'}, -70},
+		// <!-- GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA -->
+		// <!-- GREEK UPSILON WITH HOOK SYMBOL -->
+		// Last pair in second table
+		{2048, font.HintingNone, [2]rune{'\u1f4d', '\u03d2'}, -10},
+	},
 }
 
 // proprietaryFDSelectTestCases hold a sample of each font's Font Dict Select
diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go
index e7125bf..f7becca 100644
--- a/font/sfnt/sfnt.go
+++ b/font/sfnt/sfnt.go
@@ -77,6 +77,7 @@
 	errInvalidDfont           = errors.New("sfnt: invalid dfont")
 	errInvalidFont            = errors.New("sfnt: invalid font")
 	errInvalidFontCollection  = errors.New("sfnt: invalid font collection")
+	errInvalidGPOSTable       = errors.New("sfnt: invalid GPOS table")
 	errInvalidGlyphData       = errors.New("sfnt: invalid glyph data")
 	errInvalidGlyphDataLength = errors.New("sfnt: invalid glyph data length")
 	errInvalidHeadTable       = errors.New("sfnt: invalid head table")
@@ -97,11 +98,14 @@
 
 	errUnsupportedCFFFDSelectTable     = errors.New("sfnt: unsupported CFF FDSelect table")
 	errUnsupportedCFFVersion           = errors.New("sfnt: unsupported CFF version")
+	errUnsupportedClassDefFormat       = errors.New("sfnt: unsupported class definition format")
 	errUnsupportedCmapEncodings        = errors.New("sfnt: unsupported cmap encodings")
 	errUnsupportedCompoundGlyph        = errors.New("sfnt: unsupported compound glyph")
+	errUnsupportedCoverageFormat       = errors.New("sfnt: unsupported coverage format")
+	errUnsupportedExtensionPosFormat   = errors.New("sfnt: unsupported extension positioning format")
+	errUnsupportedGPOSTable            = errors.New("sfnt: unsupported GPOS table")
 	errUnsupportedGlyphDataLength      = errors.New("sfnt: unsupported glyph data length")
 	errUnsupportedKernTable            = errors.New("sfnt: unsupported kern table")
-	errUnsupportedRealNumberEncoding   = errors.New("sfnt: unsupported real number encoding")
 	errUnsupportedNumberOfCmapSegments = errors.New("sfnt: unsupported number of cmap segments")
 	errUnsupportedNumberOfFontDicts    = errors.New("sfnt: unsupported number of font dicts")
 	errUnsupportedNumberOfFonts        = errors.New("sfnt: unsupported number of fonts")
@@ -110,6 +114,7 @@
 	errUnsupportedNumberOfTables       = errors.New("sfnt: unsupported number of tables")
 	errUnsupportedPlatformEncoding     = errors.New("sfnt: unsupported platform encoding")
 	errUnsupportedPostTable            = errors.New("sfnt: unsupported post table")
+	errUnsupportedRealNumberEncoding   = errors.New("sfnt: unsupported real number encoding")
 	errUnsupportedTableOffsetLength    = errors.New("sfnt: unsupported table offset or length")
 	errUnsupportedType2Charstring      = errors.New("sfnt: unsupported Type 2 Charstring")
 )
@@ -244,6 +249,33 @@
 	return buf, nil
 }
 
+// varLenView returns bytes from the given offset for sub-tables with varying
+// length. The length of bytes is determined by staticLength plus n*itemLength,
+// where n is read as uint16 from countOffset (relative to offset). buf is an
+// optional scratch buffer (see source.view())
+func (s *source) varLenView(buf []byte, offset, staticLength, countOffset, itemLength int) ([]byte, int, error) {
+	if 0 > offset || offset > offset+staticLength {
+		return nil, 0, errInvalidBounds
+	}
+	if 0 > countOffset || countOffset+1 >= staticLength {
+		return nil, 0, errInvalidBounds
+	}
+
+	// read static part which contains our count
+	buf, err := s.view(buf, offset, staticLength)
+	if err != nil {
+		return nil, 0, err
+	}
+
+	count := int(u16(buf[countOffset:]))
+	buf, err = s.view(buf, offset, staticLength+count*itemLength)
+	if err != nil {
+		return nil, 0, err
+	}
+
+	return buf, count, nil
+}
+
 // u16 returns the uint16 in the table t at the relative offset i.
 //
 // buf is an optional scratch buffer as per the source.view method.
@@ -557,7 +589,8 @@
 	// https://www.microsoft.com/typography/otspec/otff.htm#otttables
 	// "Advanced Typographic Tables".
 	//
-	// TODO: base, gdef, gpos, gsub, jstf, math?
+	// TODO: base, gdef, gsub, jstf, math?
+	gpos table
 
 	// https://www.microsoft.com/typography/otspec/otff.htm#otttables
 	// "Other OpenType Tables".
@@ -577,6 +610,7 @@
 		isPostScript     bool
 		kernNumPairs     int32
 		kernOffset       int32
+		kernFuncs        []kernFunc
 		lineGap          int32
 		numHMetrics      int32
 		post             *PostTable
@@ -629,6 +663,10 @@
 	if err != nil {
 		return err
 	}
+	buf, kernFuncs, err := f.parseGPOSKern(buf)
+	if err != nil {
+		return err
+	}
 	buf, ascent, descent, lineGap, run, rise, numHMetrics, err := f.parseHhea(buf, numGlyphs)
 	if err != nil {
 		return err
@@ -657,6 +695,7 @@
 	f.cached.isPostScript = isPostScript
 	f.cached.kernNumPairs = kernNumPairs
 	f.cached.kernOffset = kernOffset
+	f.cached.kernFuncs = kernFuncs
 	f.cached.lineGap = lineGap
 	f.cached.numHMetrics = numHMetrics
 	f.cached.post = post
@@ -751,6 +790,8 @@
 			f.cmap = table{o, n}
 		case 0x676c7966:
 			f.glyf = table{o, n}
+		case 0x47504f53:
+			f.gpos = table{o, n}
 		case 0x68656164:
 			f.head = table{o, n}
 		case 0x68686561:
@@ -1488,10 +1529,32 @@
 //
 // It returns ErrNotFound if either glyph index is out of range.
 func (f *Font) Kern(b *Buffer, x0, x1 GlyphIndex, ppem fixed.Int26_6, h font.Hinting) (fixed.Int26_6, error) {
-	// TODO: how should this work with the GPOS table and CFF fonts?
-	// https://www.microsoft.com/typography/otspec/kern.htm says that
-	// "OpenType™ fonts containing CFF outlines are not supported by the 'kern'
-	// table and must use the 'GPOS' OpenType Layout table."
+
+	// Use GPOS kern tables if available.
+	if f.cached.kernFuncs != nil {
+		for _, kf := range f.cached.kernFuncs {
+			adv, err := kf(x0, x1)
+			if err == ErrNotFound {
+				continue
+			}
+			if err != nil {
+				return 0, err
+			}
+			kern := fixed.Int26_6(adv)
+			kern = scale(kern*ppem, f.cached.unitsPerEm)
+			if h == font.HintingFull {
+				// Quantize the fixed.Int26_6 value to the nearest pixel.
+				kern = (kern + 32) &^ 63
+			}
+			return kern, nil
+		}
+		return 0, ErrNotFound
+	}
+
+	// Fallback to kern table.
+
+	// TODO: Convert kern table handling into kernFunc and decide in Parse if
+	// GPOS or kern should be used.
 
 	if n := f.NumGlyphs(); int(x0) >= n || int(x1) >= n {
 		return 0, ErrNotFound