git.lucas.co / go_mono
git clone https://git.lucas.co/go_mono.git

commit6847effb9b768b10e85aae2c552c93fefa93c30a
parent1995ed1a25
authorNigel Tao <nigeltao@golang.org>
date2017-03-18 13:10
font/sfnt: support font collections (.ttc and .otc files).

Also add tests for Apple proprietary fonts.

Change-Id: I5ce8efa2397bb01c5255d956a77c955ba1383105
Reviewed-on: https://go-review.googlesource.com/38272
Reviewed-by: David Crawshaw <crawshaw@golang.org>

 font/sfnt/proprietary_test.go | 131 ++++++++++++++++++++++++++++++++-----
 font/sfnt/sfnt.go             | 146 ++++++++++++++++++++++++++++++++++--------
 2 files changed, 235 insertions(+), 42 deletions(-)

diff --git a/font/sfnt/proprietary_test.go b/font/sfnt/proprietary_test.go
index 9137fea..8948d5d 100644
--- a/font/sfnt/proprietary_test.go
+++ b/font/sfnt/proprietary_test.go
@@ -19,14 +19,14 @@ End User License Agreement (EULA) and a CAB format decoder. These tests assume
 that such fonts have already been installed. You may need to specify the
 directories for these fonts:
 
-go test golang.org/x/image/font/sfnt -args -proprietary -adobeDir=/foo/bar/aFonts -microsoftDir=/foo/bar/mFonts
+go test golang.org/x/image/font/sfnt -args -proprietary -adobeDir=$HOME/fonts/adobe -appleDir=$HOME/fonts/apple -microsoftDir=$HOME/fonts/microsoft
 
 To only run those tests for the Microsoft fonts:
 
-go test golang.org/x/image/font/sfnt -test.run=ProprietaryMicrosoft -args -proprietary
+go test golang.org/x/image/font/sfnt -test.run=ProprietaryMicrosoft -args -proprietary etc
 */
 
-// TODO: add Apple system fonts? Google fonts (Droid? Noto?)? Emoji fonts?
+// TODO: add Google fonts (Droid? Noto?)? Emoji fonts?
 
 // TODO: enable Apple/Microsoft tests by default on Darwin/Windows?
 
@@ -35,6 +35,8 @@ import (
 	"flag"
 	"io/ioutil"
 	"path/filepath"
+	"strconv"
+	"strings"
 	"testing"
 
 	"golang.org/x/image/font"
@@ -60,6 +62,16 @@ var (
 		"directory name for the Adobe proprietary fonts",
 	)
 
+	appleDir = flag.String(
+		"appleDir",
+		// This needs to be set explicitly. These fonts come with macOS, which
+		// is widely available but not freely available.
+		//
+		// On a Mac, set this to "/System/Library/Fonts/".
+		"",
+		"directory name for the Apple proprietary fonts",
+	)
+
 	microsoftDir = flag.String(
 		"microsoftDir",
 		"/usr/share/fonts/truetype/msttcorefonts",
@@ -87,10 +99,26 @@ func TestProprietaryAdobeSourceSansProTTF(t *testing.T) {
 	testProprietary(t, "adobe", "SourceSansPro-Regular.ttf", 1800, 54)
 }
 
+func TestProprietaryAppleAppleSymbols(t *testing.T) {
+	testProprietary(t, "apple", "Apple Symbols.ttf", 4600, -1)
+}
+
+func TestProprietaryAppleHiragino0(t *testing.T) {
+	testProprietary(t, "apple", "ヒラギノ角ゴシック W0.ttc?0", 9000, 6)
+}
+
+func TestProprietaryAppleHiragino1(t *testing.T) {
+	testProprietary(t, "apple", "ヒラギノ角ゴシック W0.ttc?1", 9000, 6)
+}
+
 func TestProprietaryMicrosoftArial(t *testing.T) {
 	testProprietary(t, "microsoft", "Arial.ttf", 1200, -1)
 }
 
+func TestProprietaryMicrosoftArialAsACollection(t *testing.T) {
+	testProprietary(t, "microsoft", "Arial.ttf?0", 1200, -1)
+}
+
 func TestProprietaryMicrosoftComicSansMS(t *testing.T) {
 	testProprietary(t, "microsoft", "Comic_Sans_MS.ttf", 550, -1)
 }
@@ -117,27 +145,55 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi
 		t.Skip("skipping proprietary font test")
 	}
 
-	file, err := []byte(nil), error(nil)
-	switch proprietor {
-	case "adobe":
-		file, err = ioutil.ReadFile(filepath.Join(*adobeDir, filename))
+	basename, fontIndex, err := filename, -1, error(nil)
+	if i := strings.IndexByte(filename, '?'); i >= 0 {
+		fontIndex, err = strconv.Atoi(filename[i+1:])
 		if err != nil {
-			t.Fatalf("%v\nPerhaps you need to set the -adobeDir=%v flag?", err, *adobeDir)
+			t.Fatalf("could not parse collection font index from filename %q", filename)
 		}
+		basename = filename[:i]
+	}
+
+	dir := ""
+	switch proprietor {
+	case "adobe":
+		dir = *adobeDir
+	case "apple":
+		dir = *appleDir
 	case "microsoft":
-		file, err = ioutil.ReadFile(filepath.Join(*microsoftDir, filename))
-		if err != nil {
-			t.Fatalf("%v\nPerhaps you need to set the -microsoftDir=%v flag?", err, *microsoftDir)
-		}
+		dir = *microsoftDir
 	default:
 		panic("unreachable")
 	}
-	f, err := Parse(file)
+	file, err := ioutil.ReadFile(filepath.Join(dir, basename))
 	if err != nil {
-		t.Fatalf("Parse: %v", err)
+		t.Fatalf("%v\nPerhaps you need to set the -%sDir flag?", err, proprietor)
 	}
-	ppem := fixed.Int26_6(f.UnitsPerEm())
 	qualifiedFilename := proprietor + "/" + filename
+
+	f := (*Font)(nil)
+	if fontIndex >= 0 {
+		c, err := ParseCollection(file)
+		if err != nil {
+			t.Fatalf("ParseCollection: %v", err)
+		}
+		if want, ok := proprietaryNumFonts[qualifiedFilename]; ok {
+			if got := c.NumFonts(); got != want {
+				t.Fatalf("NumFonts: got %d, want %d", got, want)
+			}
+		}
+		f, err = c.Font(fontIndex)
+		if err != nil {
+			t.Fatalf("Font: %v", err)
+		}
+	} else {
+		f, err = Parse(file)
+		if err != nil {
+			t.Fatalf("Parse: %v", err)
+		}
+	}
+
+	ppem := fixed.Int26_6(f.UnitsPerEm())
 	var buf Buffer
 
 	// Some of the tests below, such as which glyph index a particular rune
@@ -147,7 +203,7 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi
 	// message, but don't automatically fail (i.e. dont' call t.Fatalf).
 	gotVersion, err := f.Name(&buf, NameIDVersion)
 	if err != nil {
-		t.Fatalf("Name: %v", err)
+		t.Fatalf("Name(Version): %v", err)
 	}
 	wantVersion := proprietaryVersions[qualifiedFilename]
 	if gotVersion != wantVersion {
@@ -155,6 +211,15 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi
 			"\ngot  %q\nwant %q", gotVersion, wantVersion)
 	}
 
+	gotFull, err := f.Name(&buf, NameIDFull)
+	if err != nil {
+		t.Fatalf("Name(Full): %v", err)
+	}
+	wantFull := proprietaryFullNames[qualifiedFilename]
+	if gotFull != wantFull {
+		t.Fatalf("Name(Full):\ngot  %q\nwant %q", gotFull, wantFull)
+	}
+
 	numGlyphs := f.NumGlyphs()
 	if numGlyphs < minNumGlyphs {
 		t.Fatalf("NumGlyphs: got %d, want at least %d", numGlyphs, minNumGlyphs)
@@ -231,6 +296,15 @@ kernLoop:
 	}
 }
 
+// proprietaryNumFonts holds the expected number of fonts in each collection,
+// or 1 for a single font. It is not necessarily an exhaustive list of all
+// proprietary fonts tested.
+var proprietaryNumFonts = map[string]int{
+	"apple/ヒラギノ角ゴシック W0.ttc?0": 2,
+	"apple/ヒラギノ角ゴシック W0.ttc?1": 2,
+	"microsoft/Arial.ttf?0":      1,
+}
+
 // proprietaryVersions holds the expected version string of each proprietary
 // font tested. If third parties such as Adobe or Microsoft update their fonts,
 // and the tests subsequently fail, these versions should be updated too.
@@ -245,12 +319,37 @@ var proprietaryVersions = map[string]string{
 	"adobe/SourceSansPro-Regular.otf":   "Version 2.020;PS 2.0;hotconv 1.0.86;makeotf.lib2.5.63406",
 	"adobe/SourceSansPro-Regular.ttf":   "Version 2.020;PS 2.000;hotconv 1.0.86;makeotf.lib2.5.63406",
 
+	"apple/Apple Symbols.ttf":    "12.0d3e10",
+	"apple/ヒラギノ角ゴシック W0.ttc?0": "11.0d7e1",
+	"apple/ヒラギノ角ゴシック W0.ttc?1": "11.0d7e1",
+
 	"microsoft/Arial.ttf":           "Version 2.82",
+	"microsoft/Arial.ttf?0":         "Version 2.82",
 	"microsoft/Comic_Sans_MS.ttf":   "Version 2.10",
 	"microsoft/Times_New_Roman.ttf": "Version 2.82",
 	"microsoft/Webdings.ttf":        "Version 1.03",
 }
 
+// proprietaryFullNames holds the expected full name of each proprietary font
+// tested.
+var proprietaryFullNames = map[string]string{
+	"adobe/SourceCodePro-Regular.otf":   "Source Code Pro",
+	"adobe/SourceCodePro-Regular.ttf":   "Source Code Pro",
+	"adobe/SourceHanSansSC-Regular.otf": "Source Han Sans SC Regular",
+	"adobe/SourceSansPro-Regular.otf":   "Source Sans Pro",
+	"adobe/SourceSansPro-Regular.ttf":   "Source Sans Pro",
+
+	"apple/Apple Symbols.ttf":    "Apple Symbols",
+	"apple/ヒラギノ角ゴシック W0.ttc?0": "Hiragino Sans W0",
+	"apple/ヒラギノ角ゴシック W0.ttc?1": ".Hiragino Kaku Gothic Interface W0",
+
+	"microsoft/Arial.ttf":           "Arial",
+	"microsoft/Arial.ttf?0":         "Arial",
+	"microsoft/Comic_Sans_MS.ttf":   "Comic Sans MS",
+	"microsoft/Times_New_Roman.ttf": "Times New Roman",
+	"microsoft/Webdings.ttf":        "Webdings",
+}
+
 // proprietaryGlyphIndexTestCases hold a sample of each font's rune to glyph
 // index cmap. The numerical values can be verified by running the ttx tool.
 var proprietaryGlyphIndexTestCases = map[string]map[rune]GlyphIndex{
diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go
index e7c6670..32122bf 100644
--- a/font/sfnt/sfnt.go
+++ b/font/sfnt/sfnt.go
@@ -49,6 +49,7 @@ const (
 	maxCompoundStackSize      = 64
 	maxGlyphDataLength        = 64 * 1024
 	maxHintBits               = 256
+	maxNumFonts               = 256
 	maxNumTables              = 256
 	maxRealNumberStrLen       = 64 // Maximum length in bytes of the "-123.456E-7" representation.
 
@@ -61,22 +62,24 @@ var (
 	// ErrNotFound indicates that the requested value was not found.
 	ErrNotFound = errors.New("sfnt: not found")
 
-	errInvalidBounds        = errors.New("sfnt: invalid bounds")
-	errInvalidCFFTable      = errors.New("sfnt: invalid CFF table")
-	errInvalidCmapTable     = errors.New("sfnt: invalid cmap table")
-	errInvalidGlyphData     = errors.New("sfnt: invalid glyph data")
-	errInvalidHeadTable     = errors.New("sfnt: invalid head table")
-	errInvalidKernTable     = errors.New("sfnt: invalid kern table")
-	errInvalidLocaTable     = errors.New("sfnt: invalid loca table")
-	errInvalidLocationData  = errors.New("sfnt: invalid location data")
-	errInvalidMaxpTable     = errors.New("sfnt: invalid maxp table")
-	errInvalidNameTable     = errors.New("sfnt: invalid name table")
-	errInvalidPostTable     = errors.New("sfnt: invalid post table")
-	errInvalidSourceData    = errors.New("sfnt: invalid source data")
-	errInvalidTableOffset   = errors.New("sfnt: invalid table offset")
-	errInvalidTableTagOrder = errors.New("sfnt: invalid table tag order")
-	errInvalidUCS2String    = errors.New("sfnt: invalid UCS-2 string")
-	errInvalidVersion       = errors.New("sfnt: invalid version")
+	errInvalidBounds         = errors.New("sfnt: invalid bounds")
+	errInvalidCFFTable       = errors.New("sfnt: invalid CFF table")
+	errInvalidCmapTable      = errors.New("sfnt: invalid cmap table")
+	errInvalidFont           = errors.New("sfnt: invalid font")
+	errInvalidFontCollection = errors.New("sfnt: invalid font collection")
+	errInvalidGlyphData      = errors.New("sfnt: invalid glyph data")
+	errInvalidHeadTable      = errors.New("sfnt: invalid head table")
+	errInvalidKernTable      = errors.New("sfnt: invalid kern table")
+	errInvalidLocaTable      = errors.New("sfnt: invalid loca table")
+	errInvalidLocationData   = errors.New("sfnt: invalid location data")
+	errInvalidMaxpTable      = errors.New("sfnt: invalid maxp table")
+	errInvalidNameTable      = errors.New("sfnt: invalid name 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")
+	errInvalidTableOffset    = errors.New("sfnt: invalid table offset")
+	errInvalidTableTagOrder  = errors.New("sfnt: invalid table tag order")
+	errInvalidUCS2String     = errors.New("sfnt: invalid UCS-2 string")
 
 	errUnsupportedCFFVersion           = errors.New("sfnt: unsupported CFF version")
 	errUnsupportedCmapEncodings        = errors.New("sfnt: unsupported cmap encodings")
@@ -85,6 +88,7 @@ var (
 	errUnsupportedKernTable            = errors.New("sfnt: unsupported kern table")
 	errUnsupportedRealNumberEncoding   = errors.New("sfnt: unsupported real number encoding")
 	errUnsupportedNumberOfCmapSegments = errors.New("sfnt: unsupported number of cmap segments")
+	errUnsupportedNumberOfFonts        = errors.New("sfnt: unsupported number of fonts")
 	errUnsupportedNumberOfHints        = errors.New("sfnt: unsupported number of hints")
 	errUnsupportedNumberOfTables       = errors.New("sfnt: unsupported number of tables")
 	errUnsupportedPlatformEncoding     = errors.New("sfnt: unsupported platform encoding")
@@ -256,19 +260,105 @@ type table struct {
 	offset, length uint32
 }
 
-// Parse parses an SFNT font from a []byte data source.
+// ParseCollection parses an SFNT font collection, such as TTC or OTC data,
+// from a []byte data source.
+//
+// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
+// will return a collection containing 1 font.
+func ParseCollection(src []byte) (*Collection, error) {
+	c := &Collection{src: source{b: src}}
+	if err := c.initialize(); err != nil {
+		return nil, err
+	}
+	return c, nil
+}
+
+// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data,
+// from an io.ReaderAt data source.
+//
+// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
+// will return a collection containing 1 font.
+func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
+	c := &Collection{src: source{r: src}}
+	if err := c.initialize(); err != nil {
+		return nil, err
+	}
+	return c, nil
+}
+
+// Collection is a collection of one or more fonts.
+//
+// All of the Collection methods are safe to call concurrently.
+type Collection struct {
+	src     source
+	offsets []uint32
+}
+
+// NumFonts returns the number of fonts in the collection.
+func (c *Collection) NumFonts() int { return len(c.offsets) }
+
+func (c *Collection) initialize() error {
+	// The https://www.microsoft.com/typography/otspec/otff.htm "Font
+	// Collections" section describes the TTC Header.
+	buf, err := c.src.view(nil, 0, 12)
+	if err != nil {
+		return err
+	}
+	// These cases match the switch statement in Font.initializeTables.
+	switch u32(buf) {
+	default:
+		return errInvalidFontCollection
+	case 0x00010000, 0x4f54544f:
+		// Try parsing it as a single font instead of a collection.
+		c.offsets = []uint32{0}
+	case 0x74746366: // "ttcf".
+		numFonts := u32(buf[8:])
+		if numFonts == 0 || numFonts > maxNumFonts {
+			return errUnsupportedNumberOfFonts
+		}
+		buf, err = c.src.view(nil, 12, int(4*numFonts))
+		if err != nil {
+			return err
+		}
+		c.offsets = make([]uint32, numFonts)
+		for i := range c.offsets {
+			o := u32(buf[4*i:])
+			if o > maxTableOffset {
+				return errUnsupportedTableOffsetLength
+			}
+			c.offsets[i] = o
+		}
+	}
+	return nil
+}
+
+// Font returns the i'th font in the collection.
+func (c *Collection) Font(i int) (*Font, error) {
+	if i < 0 || len(c.offsets) <= i {
+		return nil, ErrNotFound
+	}
+	f := &Font{src: c.src}
+	if err := f.initialize(int(c.offsets[i])); err != nil {
+		return nil, err
+	}
+	return f, nil
+}
+
+// Parse parses an SFNT font, such as TTF or OTF data, from a []byte data
+// source.
 func Parse(src []byte) (*Font, error) {
 	f := &Font{src: source{b: src}}
-	if err := f.initialize(); err != nil {
+	if err := f.initialize(0); err != nil {
 		return nil, err
 	}
 	return f, nil
 }
 
-// ParseReaderAt parses an SFNT font from an io.ReaderAt data source.
+// ParseReaderAt parses an SFNT font, such as TTF or OTF data, from an
+// io.ReaderAt data source.
 func ParseReaderAt(src io.ReaderAt) (*Font, error) {
 	f := &Font{src: source{r: src}}
-	if err := f.initialize(); err != nil {
+	if err := f.initialize(0); err != nil {
 		return nil, err
 	}
 	return f, nil
@@ -362,11 +452,11 @@ func (f *Font) NumGlyphs() int { return len(f.cached.locations) - 1 }
 // UnitsPerEm returns the number of units per em for f.
 func (f *Font) UnitsPerEm() Units { return f.cached.unitsPerEm }
 
-func (f *Font) initialize() error {
+func (f *Font) initialize(offset int) error {
 	if !f.src.valid() {
 		return errInvalidSourceData
 	}
-	buf, isPostScript, err := f.initializeTables(nil)
+	buf, isPostScript, err := f.initializeTables(offset)
 	if err != nil {
 		return err
 	}
@@ -412,21 +502,25 @@ func (f *Font) initialize() error {
 	return nil
 }
 
-func (f *Font) initializeTables(buf []byte) (buf1 []byte, isPostScript bool, err error) {
+func (f *Font) initializeTables(offset int) (buf1 []byte, isPostScript bool, err error) {
 	// https://www.microsoft.com/typography/otspec/otff.htm "Organization of an
 	// OpenType Font" says that "The OpenType font starts with the Offset
 	// Table", which is 12 bytes.
-	buf, err = f.src.view(buf, 0, 12)
+	buf, err := f.src.view(nil, offset, 12)
 	if err != nil {
 		return nil, false, err
 	}
+	// When updating the cases in this switch statement, also update the
+	// Collection.initialize method.
 	switch u32(buf) {
 	default:
-		return nil, false, errInvalidVersion
+		return nil, false, errInvalidFont
 	case 0x00010000:
 		// No-op.
 	case 0x4f54544f: // "OTTO".
 		isPostScript = true
+	case 0x74746366: // "ttcf".
+		return nil, false, errInvalidSingleFont
 	}
 	numTables := int(u16(buf[4:]))
 	if numTables > maxNumTables {
@@ -435,7 +529,7 @@ func (f *Font) initializeTables(buf []byte) (buf1 []byte, isPostScript bool, err
 
 	// "The Offset Table is followed immediately by the Table Record entries...
 	// sorted in ascending order by tag", 16 bytes each.
-	buf, err = f.src.view(buf, 12, 16*numTables)
+	buf, err = f.src.view(buf, offset+12, 16*numTables)
 	if err != nil {
 		return nil, false, err
 	}