font/sfnt: support cmap format 12.

Change-Id: I2791f5aec860bbb16c6c6945703827afd55c11dc
Reviewed-on: https://go-review.googlesource.com/36291
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/font/sfnt/cmap.go b/font/sfnt/cmap.go
index be302d2..3adda03 100644
--- a/font/sfnt/cmap.go
+++ b/font/sfnt/cmap.go
@@ -80,7 +80,7 @@
 	case 4:
 		return true
 	case 12:
-		// TODO: implement.
+		return true
 	}
 	return false
 }
@@ -92,7 +92,7 @@
 	case 4:
 		return f.makeCachedGlyphIndexFormat4(buf, offset, length)
 	case 12:
-		// TODO: implement, including a cmapEntry32 type (32, not 16).
+		return f.makeCachedGlyphIndexFormat12(buf, offset, length)
 	}
 	panic("unreachable")
 }
@@ -196,6 +196,68 @@
 	return buf, nil
 }
 
+func (f *Font) makeCachedGlyphIndexFormat12(buf []byte, offset, _ uint32) ([]byte, error) {
+	const headerSize = 16
+	if offset+headerSize > f.cmap.length {
+		return nil, errInvalidCmapTable
+	}
+	var err error
+	buf, err = f.src.view(buf, int(f.cmap.offset+offset), headerSize)
+	if err != nil {
+		return nil, err
+	}
+	offset += headerSize
+
+	length := u32(buf[4:])
+	numGroups := u32(buf[12:])
+	if f.cmap.length < offset || length > f.cmap.length-offset {
+		return nil, errInvalidCmapTable
+	}
+	if numGroups > maxCmapSegments {
+		return nil, errUnsupportedNumberOfCmapSegments
+	}
+
+	eLength := 12 * numGroups
+	if headerSize+eLength != length {
+		return nil, errInvalidCmapTable
+	}
+	buf, err = f.src.view(buf, int(f.cmap.offset+offset), int(eLength))
+	if err != nil {
+		return nil, err
+	}
+	offset += eLength
+
+	entries := make([]cmapEntry32, numGroups)
+	for i := range entries {
+		entries[i] = cmapEntry32{
+			start: u32(buf[0+12*i:]),
+			end:   u32(buf[4+12*i:]),
+			delta: u32(buf[8+12*i:]),
+		}
+	}
+
+	f.cached.glyphIndex = func(f *Font, b *Buffer, r rune) (GlyphIndex, error) {
+		c := uint32(r)
+		for i, j := 0, len(entries); i < j; {
+			h := i + (j-i)/2
+			entry := &entries[h]
+			if c < entry.start {
+				j = h
+			} else if entry.end < c {
+				i = h + 1
+			} else {
+				return GlyphIndex(c - entry.start + entry.delta), nil
+			}
+		}
+		return 0, nil
+	}
+	return buf, nil
+}
+
 type cmapEntry16 struct {
 	end, start, delta, offset uint16
 }
+
+type cmapEntry32 struct {
+	start, end, delta uint32
+}
diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go
index f1c6ac1..c68d78c 100644
--- a/font/sfnt/sfnt_test.go
+++ b/font/sfnt/sfnt_test.go
@@ -94,7 +94,7 @@
 		t.Fatal(err)
 	}
 
-	for _, format := range []int{-1, 0, 4} {
+	for _, format := range []int{-1, 0, 4, 12} {
 		testGlyphIndex(t, data, format)
 	}
 }
@@ -158,26 +158,26 @@
 		{'\u4e2d', 12},
 		{'\u4e2e', 0},
 
-		/*
-			TODO: support runes above U+FFFF, i.e. cmap format 12.
+		{'\U0001f0a0', 0},
+		{'\U0001f0a1', 13},
+		{'\U0001f0a2', 0},
 
-			{'\U0001f0a0', 0},
-			{'\U0001f0a1', 13},
-			{'\U0001f0a2', 0},
-
-			{'\U0001f0b0', 0},
-			{'\U0001f0b1', 14},
-			{'\U0001f0b2', 15},
-			{'\U0001f0b3', 0},
-		*/
+		{'\U0001f0b0', 0},
+		{'\U0001f0b1', 14},
+		{'\U0001f0b2', 15},
+		{'\U0001f0b3', 0},
 	}
 
 	var b Buffer
 	for _, tc := range testCases {
 		want := tc.want
-		// cmap format 0, with the Macintosh Roman encoding, can only represent
-		// a limited set of non-ASCII runes, e.g. U+00FF.
-		if cmapFormat == 0 && tc.r > '\u007f' && tc.r != '\u00ff' {
+		switch {
+		case cmapFormat == 0 && tc.r > '\u007f' && tc.r != '\u00ff':
+			// cmap format 0, with the Macintosh Roman encoding, can only
+			// represent a limited set of non-ASCII runes, e.g. U+00FF.
+			want = 0
+		case cmapFormat == 4 && tc.r > '\uffff':
+			// cmap format 4 only supports the Basic Multilingual Plane (BMP).
 			want = 0
 		}