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>
This commit is contained in:
Nigel Tao 2017-03-18 13:10:39 +11:00
parent 1995ed1a25
commit 6847effb9b
2 changed files with 237 additions and 44 deletions

View File

@ -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 that such fonts have already been installed. You may need to specify the
directories for these fonts: 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: 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? // TODO: enable Apple/Microsoft tests by default on Darwin/Windows?
@ -35,6 +35,8 @@ import (
"flag" "flag"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"strconv"
"strings"
"testing" "testing"
"golang.org/x/image/font" "golang.org/x/image/font"
@ -60,6 +62,16 @@ var (
"directory name for the Adobe proprietary fonts", "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 = flag.String(
"microsoftDir", "microsoftDir",
"/usr/share/fonts/truetype/msttcorefonts", "/usr/share/fonts/truetype/msttcorefonts",
@ -87,10 +99,26 @@ func TestProprietaryAdobeSourceSansProTTF(t *testing.T) {
testProprietary(t, "adobe", "SourceSansPro-Regular.ttf", 1800, 54) 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) { func TestProprietaryMicrosoftArial(t *testing.T) {
testProprietary(t, "microsoft", "Arial.ttf", 1200, -1) 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) { func TestProprietaryMicrosoftComicSansMS(t *testing.T) {
testProprietary(t, "microsoft", "Comic_Sans_MS.ttf", 550, -1) 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") t.Skip("skipping proprietary font test")
} }
file, err := []byte(nil), error(nil) 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("could not parse collection font index from filename %q", filename)
}
basename = filename[:i]
}
dir := ""
switch proprietor { switch proprietor {
case "adobe": case "adobe":
file, err = ioutil.ReadFile(filepath.Join(*adobeDir, filename)) dir = *adobeDir
if err != nil { case "apple":
t.Fatalf("%v\nPerhaps you need to set the -adobeDir=%v flag?", err, *adobeDir) dir = *appleDir
}
case "microsoft": case "microsoft":
file, err = ioutil.ReadFile(filepath.Join(*microsoftDir, filename)) dir = *microsoftDir
if err != nil {
t.Fatalf("%v\nPerhaps you need to set the -microsoftDir=%v flag?", err, *microsoftDir)
}
default: default:
panic("unreachable") panic("unreachable")
} }
f, err := Parse(file) file, err := ioutil.ReadFile(filepath.Join(dir, basename))
if err != nil { 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 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 var buf Buffer
// Some of the tests below, such as which glyph index a particular rune // 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). // message, but don't automatically fail (i.e. dont' call t.Fatalf).
gotVersion, err := f.Name(&buf, NameIDVersion) gotVersion, err := f.Name(&buf, NameIDVersion)
if err != nil { if err != nil {
t.Fatalf("Name: %v", err) t.Fatalf("Name(Version): %v", err)
} }
wantVersion := proprietaryVersions[qualifiedFilename] wantVersion := proprietaryVersions[qualifiedFilename]
if gotVersion != wantVersion { if gotVersion != wantVersion {
@ -155,6 +211,15 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi
"\ngot %q\nwant %q", gotVersion, wantVersion) "\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() numGlyphs := f.NumGlyphs()
if numGlyphs < minNumGlyphs { if numGlyphs < minNumGlyphs {
t.Fatalf("NumGlyphs: got %d, want at least %d", 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 // proprietaryVersions holds the expected version string of each proprietary
// font tested. If third parties such as Adobe or Microsoft update their fonts, // font tested. If third parties such as Adobe or Microsoft update their fonts,
// and the tests subsequently fail, these versions should be updated too. // 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.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", "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": "Version 2.82",
"microsoft/Arial.ttf?0": "Version 2.82",
"microsoft/Comic_Sans_MS.ttf": "Version 2.10", "microsoft/Comic_Sans_MS.ttf": "Version 2.10",
"microsoft/Times_New_Roman.ttf": "Version 2.82", "microsoft/Times_New_Roman.ttf": "Version 2.82",
"microsoft/Webdings.ttf": "Version 1.03", "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 // proprietaryGlyphIndexTestCases hold a sample of each font's rune to glyph
// index cmap. The numerical values can be verified by running the ttx tool. // index cmap. The numerical values can be verified by running the ttx tool.
var proprietaryGlyphIndexTestCases = map[string]map[rune]GlyphIndex{ var proprietaryGlyphIndexTestCases = map[string]map[rune]GlyphIndex{

View File

@ -49,6 +49,7 @@ const (
maxCompoundStackSize = 64 maxCompoundStackSize = 64
maxGlyphDataLength = 64 * 1024 maxGlyphDataLength = 64 * 1024
maxHintBits = 256 maxHintBits = 256
maxNumFonts = 256
maxNumTables = 256 maxNumTables = 256
maxRealNumberStrLen = 64 // Maximum length in bytes of the "-123.456E-7" representation. 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 indicates that the requested value was not found.
ErrNotFound = errors.New("sfnt: not found") ErrNotFound = errors.New("sfnt: not found")
errInvalidBounds = errors.New("sfnt: invalid bounds") errInvalidBounds = errors.New("sfnt: invalid bounds")
errInvalidCFFTable = errors.New("sfnt: invalid CFF table") errInvalidCFFTable = errors.New("sfnt: invalid CFF table")
errInvalidCmapTable = errors.New("sfnt: invalid cmap table") errInvalidCmapTable = errors.New("sfnt: invalid cmap table")
errInvalidGlyphData = errors.New("sfnt: invalid glyph data") errInvalidFont = errors.New("sfnt: invalid font")
errInvalidHeadTable = errors.New("sfnt: invalid head table") errInvalidFontCollection = errors.New("sfnt: invalid font collection")
errInvalidKernTable = errors.New("sfnt: invalid kern table") errInvalidGlyphData = errors.New("sfnt: invalid glyph data")
errInvalidLocaTable = errors.New("sfnt: invalid loca table") errInvalidHeadTable = errors.New("sfnt: invalid head table")
errInvalidLocationData = errors.New("sfnt: invalid location data") errInvalidKernTable = errors.New("sfnt: invalid kern table")
errInvalidMaxpTable = errors.New("sfnt: invalid maxp table") errInvalidLocaTable = errors.New("sfnt: invalid loca table")
errInvalidNameTable = errors.New("sfnt: invalid name table") errInvalidLocationData = errors.New("sfnt: invalid location data")
errInvalidPostTable = errors.New("sfnt: invalid post table") errInvalidMaxpTable = errors.New("sfnt: invalid maxp table")
errInvalidSourceData = errors.New("sfnt: invalid source data") errInvalidNameTable = errors.New("sfnt: invalid name table")
errInvalidTableOffset = errors.New("sfnt: invalid table offset") errInvalidPostTable = errors.New("sfnt: invalid post table")
errInvalidTableTagOrder = errors.New("sfnt: invalid table tag order") errInvalidSingleFont = errors.New("sfnt: invalid single font (data is a font collection)")
errInvalidUCS2String = errors.New("sfnt: invalid UCS-2 string") errInvalidSourceData = errors.New("sfnt: invalid source data")
errInvalidVersion = errors.New("sfnt: invalid version") 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") errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version")
errUnsupportedCmapEncodings = errors.New("sfnt: unsupported cmap encodings") errUnsupportedCmapEncodings = errors.New("sfnt: unsupported cmap encodings")
@ -85,6 +88,7 @@ var (
errUnsupportedKernTable = errors.New("sfnt: unsupported kern table") errUnsupportedKernTable = errors.New("sfnt: unsupported kern table")
errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding") errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding")
errUnsupportedNumberOfCmapSegments = errors.New("sfnt: unsupported number of cmap segments") 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") errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints")
errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables") errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables")
errUnsupportedPlatformEncoding = errors.New("sfnt: unsupported platform encoding") errUnsupportedPlatformEncoding = errors.New("sfnt: unsupported platform encoding")
@ -256,19 +260,105 @@ type table struct {
offset, length uint32 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,
func Parse(src []byte) (*Font, error) { // from a []byte data source.
f := &Font{src: source{b: src}} //
if err := f.initialize(); err != nil { // 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 nil, err
} }
return f, nil return f, nil
} }
// ParseReaderAt parses an SFNT font from an io.ReaderAt data source. // 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(0); err != nil {
return nil, err
}
return f, nil
}
// ParseReaderAt parses an SFNT font, such as TTF or OTF data, from an
// io.ReaderAt data source.
func ParseReaderAt(src io.ReaderAt) (*Font, error) { func ParseReaderAt(src io.ReaderAt) (*Font, error) {
f := &Font{src: source{r: src}} f := &Font{src: source{r: src}}
if err := f.initialize(); err != nil { if err := f.initialize(0); err != nil {
return nil, err return nil, err
} }
return f, nil 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. // UnitsPerEm returns the number of units per em for f.
func (f *Font) UnitsPerEm() Units { return f.cached.unitsPerEm } 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() { if !f.src.valid() {
return errInvalidSourceData return errInvalidSourceData
} }
buf, isPostScript, err := f.initializeTables(nil) buf, isPostScript, err := f.initializeTables(offset)
if err != nil { if err != nil {
return err return err
} }
@ -412,21 +502,25 @@ func (f *Font) initialize() error {
return nil 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 // https://www.microsoft.com/typography/otspec/otff.htm "Organization of an
// OpenType Font" says that "The OpenType font starts with the Offset // OpenType Font" says that "The OpenType font starts with the Offset
// Table", which is 12 bytes. // Table", which is 12 bytes.
buf, err = f.src.view(buf, 0, 12) buf, err := f.src.view(nil, offset, 12)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
// When updating the cases in this switch statement, also update the
// Collection.initialize method.
switch u32(buf) { switch u32(buf) {
default: default:
return nil, false, errInvalidVersion return nil, false, errInvalidFont
case 0x00010000: case 0x00010000:
// No-op. // No-op.
case 0x4f54544f: // "OTTO". case 0x4f54544f: // "OTTO".
isPostScript = true isPostScript = true
case 0x74746366: // "ttcf".
return nil, false, errInvalidSingleFont
} }
numTables := int(u16(buf[4:])) numTables := int(u16(buf[4:]))
if numTables > maxNumTables { 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... // "The Offset Table is followed immediately by the Table Record entries...
// sorted in ascending order by tag", 16 bytes each. // 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 { if err != nil {
return nil, false, err return nil, false, err
} }