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 @@ func TestProprietaryAppleGeezaPro1(t *testing.T) { 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 @@ kernLoop: // 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 @@ var proprietaryVersions = map[string]string{ "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 @@ var proprietaryFullNames = map[string]string{ "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 @@ var proprietaryGlyphIndexTestCases = map[string]map[rune]GlyphIndex{ '\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 @@ var proprietaryGlyphTestCases = map[string]map[rune][]Segment{ }, }, + "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 @@ var ( 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 @@ func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) { 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) 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) + // 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 @@ func (c *Collection) initialize() error { 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 @@ func (c *Collection) initialize() error { 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 @@ func (c *Collection) Font(i int) (*Font, error) { // 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 @@ func Parse(src []byte) (*Font, error) { // 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 @@ func (f *Font) NumGlyphs() int { return len(f.cached.glyphData.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(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 @@ func (f *Font) initialize(offset int) error { 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 @@ func (f *Font) initializeTables(offset int) (buf1 []byte, isPostScript bool, err 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 @@ func (f *Font) initializeTables(offset int) (buf1 []byte, isPostScript bool, err 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 }