font/sfnt: support .dfont files.

Change-Id: Id7aa18c48c65586c688cee230ce87f4d88dae9b5
Reviewed-on: https://go-review.googlesource.com/40893
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/font/sfnt/proprietary_test.go b/font/sfnt/proprietary_test.go
index f0917d6..2df6f6c 100644
--- a/font/sfnt/proprietary_test.go
+++ b/font/sfnt/proprietary_test.go
@@ -111,6 +111,30 @@
 	testProprietary(t, "apple", "GeezaPro.ttc?1", 1700, -1)
 }
 
+func TestProprietaryAppleHelvetica0(t *testing.T) {
+	testProprietary(t, "apple", "Helvetica.dfont?0", 2100, -1)
+}
+
+func TestProprietaryAppleHelvetica1(t *testing.T) {
+	testProprietary(t, "apple", "Helvetica.dfont?1", 2100, -1)
+}
+
+func TestProprietaryAppleHelvetica2(t *testing.T) {
+	testProprietary(t, "apple", "Helvetica.dfont?2", 2100, -1)
+}
+
+func TestProprietaryAppleHelvetica3(t *testing.T) {
+	testProprietary(t, "apple", "Helvetica.dfont?3", 2100, -1)
+}
+
+func TestProprietaryAppleHelvetica4(t *testing.T) {
+	testProprietary(t, "apple", "Helvetica.dfont?4", 1300, -1)
+}
+
+func TestProprietaryAppleHelvetica5(t *testing.T) {
+	testProprietary(t, "apple", "Helvetica.dfont?5", 1300, -1)
+}
+
 func TestProprietaryAppleHiragino0(t *testing.T) {
 	testProprietary(t, "apple", "ヒラギノ角ゴシック W0.ttc?0", 9000, -1)
 }
@@ -320,8 +344,8 @@
 // or 1 for a single font. It is not necessarily an exhaustive list of all
 // proprietary fonts tested.
 var proprietaryNumFonts = map[string]int{
+	"apple/Helvetica.dfont?0":    6,
 	"apple/ヒラギノ角ゴシック W0.ttc?0": 2,
-	"apple/ヒラギノ角ゴシック W0.ttc?1": 2,
 	"microsoft/Arial.ttf?0":      1,
 }
 
@@ -342,6 +366,12 @@
 	"apple/Apple Symbols.ttf":    "12.0d3e10",
 	"apple/GeezaPro.ttc?0":       "12.0d1e3",
 	"apple/GeezaPro.ttc?1":       "12.0d1e3",
+	"apple/Helvetica.dfont?0":    "12.0d1e3",
+	"apple/Helvetica.dfont?1":    "12.0d1e3",
+	"apple/Helvetica.dfont?2":    "12.0d1e3",
+	"apple/Helvetica.dfont?3":    "12.0d1e3",
+	"apple/Helvetica.dfont?4":    "12.0d1e3",
+	"apple/Helvetica.dfont?5":    "12.0d1e3",
 	"apple/ヒラギノ角ゴシック W0.ttc?0": "11.0d7e1",
 	"apple/ヒラギノ角ゴシック W0.ttc?1": "11.0d7e1",
 
@@ -364,6 +394,12 @@
 	"apple/Apple Symbols.ttf":    "Apple Symbols",
 	"apple/GeezaPro.ttc?0":       "Geeza Pro Regular",
 	"apple/GeezaPro.ttc?1":       "Geeza Pro Bold",
+	"apple/Helvetica.dfont?0":    "Helvetica",
+	"apple/Helvetica.dfont?1":    "Helvetica Bold",
+	"apple/Helvetica.dfont?2":    "Helvetica Oblique",
+	"apple/Helvetica.dfont?3":    "Helvetica Bold Oblique",
+	"apple/Helvetica.dfont?4":    "Helvetica Light",
+	"apple/Helvetica.dfont?5":    "Helvetica Light Oblique",
 	"apple/ヒラギノ角ゴシック W0.ttc?0": "Hiragino Sans W0",
 	"apple/ヒラギノ角ゴシック W0.ttc?1": ".Hiragino Kaku Gothic Interface W0",
 
@@ -422,6 +458,17 @@
 		'\u2030': 1728, // U+2030 PER MILLE SIGN
 	},
 
+	"apple/Helvetica.dfont?0": {
+		'\u0041':     36,   // U+0041 LATIN CAPITAL LETTER A
+		'\u00f1':     120,  // U+00F1 LATIN SMALL LETTER N WITH TILDE
+		'\u0401':     473,  // U+0401 CYRILLIC CAPITAL LETTER IO
+		'\u200d':     611,  // U+200D ZERO WIDTH JOINER
+		'\u20ab':     1743, // U+20AB DONG SIGN
+		'\u2229':     0,    // U+2229 INTERSECTION
+		'\u04e9':     1208, // U+04E9 CYRILLIC SMALL LETTER BARRED O
+		'\U0001f100': 0,    // U+0001F100 DIGIT ZERO FULL STOP
+	},
+
 	"microsoft/Arial.ttf": {
 		'\u0041':     36,   // U+0041 LATIN CAPITAL LETTER A
 		'\u00f1':     120,  // U+00F1 LATIN SMALL LETTER N WITH TILDE
@@ -849,6 +896,40 @@
 		},
 	},
 
+	"apple/Helvetica.dfont?0": {
+		'i': {
+			// - contour #0
+			moveTo(132, 1066),
+			lineTo(315, 1066),
+			lineTo(315, 0),
+			lineTo(132, 0),
+			lineTo(132, 1066),
+			// - contour #1
+			moveTo(132, 1469),
+			lineTo(315, 1469),
+			lineTo(315, 1265),
+			lineTo(132, 1265),
+			lineTo(132, 1469),
+		},
+	},
+
+	"apple/Helvetica.dfont?1": {
+		'i': {
+			// - contour #0
+			moveTo(426, 1220),
+			lineTo(137, 1220),
+			lineTo(137, 1483),
+			lineTo(426, 1483),
+			lineTo(426, 1220),
+			// - contour #1
+			moveTo(137, 1090),
+			lineTo(426, 1090),
+			lineTo(426, 0),
+			lineTo(137, 0),
+			lineTo(137, 1090),
+		},
+	},
+
 	"microsoft/Arial.ttf": {
 		',': {
 			// - contour #0
diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go
index 3d33e3b..5cb1225 100644
--- a/font/sfnt/sfnt.go
+++ b/font/sfnt/sfnt.go
@@ -70,6 +70,7 @@
 	errInvalidBounds          = errors.New("sfnt: invalid bounds")
 	errInvalidCFFTable        = errors.New("sfnt: invalid CFF table")
 	errInvalidCmapTable       = errors.New("sfnt: invalid cmap table")
+	errInvalidDfont           = errors.New("sfnt: invalid dfont")
 	errInvalidFont            = errors.New("sfnt: invalid font")
 	errInvalidFontCollection  = errors.New("sfnt: invalid font collection")
 	errInvalidGlyphData       = errors.New("sfnt: invalid glyph data")
@@ -303,6 +304,7 @@
 type Collection struct {
 	src     source
 	offsets []uint32
+	isDfont bool
 }
 
 // NumFonts returns the number of fonts in the collection.
@@ -310,8 +312,13 @@
 
 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)
+	// Collections" section describes the TTC header.
+	//
+	// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
+	// describes the dfont header.
+	//
+	// 16 is the maximum of sizeof(TTCHeader) and sizeof(DfontHeader).
+	buf, err := c.src.view(nil, 0, 16)
 	if err != nil {
 		return err
 	}
@@ -319,6 +326,8 @@
 	switch u32(buf) {
 	default:
 		return errInvalidFontCollection
+	case dfontResourceDataOffset:
+		return c.parseDfont(buf, u32(buf[4:]), u32(buf[12:]))
 	case 0x00010000, 0x4f54544f:
 		// Try parsing it as a single font instead of a collection.
 		c.offsets = []uint32{0}
@@ -343,13 +352,116 @@
 	return nil
 }
 
+// dfontResourceDataOffset is the assumed value of a dfont file's resource data
+// offset.
+//
+// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
+// says that "A Mac OS resource file... [starts with an] offset from start of
+// file to start of resource data section... [usually] 0x0100". In theory,
+// 0x00000100 isn't always a magic number for identifying dfont files. In
+// practice, it seems to work.
+const dfontResourceDataOffset = 0x00000100
+
+// parseDfont parses a dfont resource map, as per
+// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
+//
+// That unofficial wiki page lists all of its fields as *signed* integers,
+// which looks unusual. The actual file format might use *unsigned* integers in
+// various places, but until we have either an official specification or an
+// actual dfont file where this matters, we'll use signed integers and treat
+// negative values as invalid.
+func (c *Collection) parseDfont(buf []byte, resourceMapOffset, resourceMapLength uint32) error {
+	if resourceMapOffset > maxTableOffset || resourceMapLength > maxTableLength {
+		return errUnsupportedTableOffsetLength
+	}
+
+	const headerSize = 28
+	if resourceMapLength < headerSize {
+		return errInvalidDfont
+	}
+	buf, err := c.src.view(buf, int(resourceMapOffset+24), 2)
+	if err != nil {
+		return err
+	}
+	typeListOffset := int(int16(u16(buf)))
+
+	if typeListOffset < headerSize || resourceMapLength < uint32(typeListOffset)+2 {
+		return errInvalidDfont
+	}
+	buf, err = c.src.view(buf, int(resourceMapOffset)+typeListOffset, 2)
+	if err != nil {
+		return err
+	}
+	typeCount := int(int16(u16(buf)))
+
+	const tSize = 8
+	if typeCount < 0 || tSize*uint32(typeCount) > resourceMapLength-uint32(typeListOffset)-2 {
+		return errInvalidDfont
+	}
+	buf, err = c.src.view(buf, int(resourceMapOffset)+typeListOffset+2, tSize*typeCount)
+	if err != nil {
+		return err
+	}
+	resourceCount, resourceListOffset := 0, 0
+	for i := 0; i < typeCount; i++ {
+		if u32(buf[tSize*i:]) != 0x73666e74 { // "sfnt".
+			continue
+		}
+
+		resourceCount = int(int16(u16(buf[tSize*i+4:])))
+		if resourceCount < 0 {
+			return errInvalidDfont
+		}
+		// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
+		// says that the value in the wire format is "the number of
+		// resources of this type, minus one."
+		resourceCount++
+
+		resourceListOffset = int(int16(u16(buf[tSize*i+6:])))
+		if resourceListOffset < 0 {
+			return errInvalidDfont
+		}
+		break
+	}
+	if resourceCount == 0 {
+		return errInvalidDfont
+	}
+	if resourceCount > maxNumFonts {
+		return errUnsupportedNumberOfFonts
+	}
+
+	const rSize = 12
+	if o, n := uint32(typeListOffset+resourceListOffset), rSize*uint32(resourceCount); o > resourceMapLength || n > resourceMapLength-o {
+		return errInvalidDfont
+	} else {
+		buf, err = c.src.view(buf, int(resourceMapOffset+o), int(n))
+		if err != nil {
+			return err
+		}
+	}
+	c.offsets = make([]uint32, resourceCount)
+	for i := range c.offsets {
+		o := 0xffffff & u32(buf[rSize*i+4:])
+		// Offsets are relative to the resource data start, not the file start.
+		// A particular resource's data also starts with a 4-byte length, which
+		// we skip.
+		o += dfontResourceDataOffset + 4
+		if o > maxTableOffset {
+			return errUnsupportedTableOffsetLength
+		}
+		c.offsets[i] = o
+	}
+	c.isDfont = true
+	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 {
+	if err := f.initialize(int(c.offsets[i]), c.isDfont); err != nil {
 		return nil, err
 	}
 	return f, nil
@@ -359,7 +471,7 @@
 // source.
 func Parse(src []byte) (*Font, error) {
 	f := &Font{src: source{b: src}}
-	if err := f.initialize(0); err != nil {
+	if err := f.initialize(0, false); err != nil {
 		return nil, err
 	}
 	return f, nil
@@ -369,7 +481,7 @@
 // io.ReaderAt data source.
 func ParseReaderAt(src io.ReaderAt) (*Font, error) {
 	f := &Font{src: source{r: src}}
-	if err := f.initialize(0); err != nil {
+	if err := f.initialize(0, false); err != nil {
 		return nil, err
 	}
 	return f, nil
@@ -462,11 +574,11 @@
 // UnitsPerEm returns the number of units per em for f.
 func (f *Font) UnitsPerEm() Units { return f.cached.unitsPerEm }
 
-func (f *Font) initialize(offset int) error {
+func (f *Font) initialize(offset int, isDfont bool) error {
 	if !f.src.valid() {
 		return errInvalidSourceData
 	}
-	buf, isPostScript, err := f.initializeTables(offset)
+	buf, isPostScript, err := f.initializeTables(offset, isDfont)
 	if err != nil {
 		return err
 	}
@@ -526,7 +638,7 @@
 	return nil
 }
 
-func (f *Font) initializeTables(offset int) (buf1 []byte, isPostScript bool, err error) {
+func (f *Font) initializeTables(offset int, isDfont bool) (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.
@@ -539,6 +651,8 @@
 	switch u32(buf) {
 	default:
 		return nil, false, errInvalidFont
+	case dfontResourceDataOffset:
+		return nil, false, errInvalidSingleFont
 	case 0x00010000:
 		// No-op.
 	case 0x4f54544f: // "OTTO".
@@ -567,6 +681,15 @@
 		prevTag = tag
 
 		o, n := u32(b[8:12]), u32(b[12:16])
+		// For dfont files, the offset is relative to the resource, not the
+		// file.
+		if isDfont {
+			origO := o
+			o += uint32(offset)
+			if o < origO {
+				return nil, false, errUnsupportedTableOffsetLength
+			}
+		}
 		if o > maxTableOffset || n > maxTableLength {
 			return nil, false, errUnsupportedTableOffsetLength
 		}