font/sfnt: add ErrColoredGlyph.

Also add tests for the Noto proprietary fonts. Prior to this commit,
NotoColorEmoji.ttf was unsupported. It's still not well supported, but
the error message returned is now more informative.

Change-Id: I61a3301d7f2458a4b838eb1de7a73d6472e3486f
Reviewed-on: https://go-review.googlesource.com/43694
Reviewed-by: David Crawshaw <crawshaw@golang.org>
This commit is contained in:
Nigel Tao 2017-05-20 18:22:12 +10:00
parent f483456c9f
commit d835a09709
2 changed files with 83 additions and 18 deletions

View File

@ -23,15 +23,14 @@ go test golang.org/x/image/font/sfnt -args -proprietary \
-adobeDir=$HOME/fonts/adobe \ -adobeDir=$HOME/fonts/adobe \
-appleDir=$HOME/fonts/apple \ -appleDir=$HOME/fonts/apple \
-dejavuDir=$HOME/fonts/dejavu \ -dejavuDir=$HOME/fonts/dejavu \
-microsoftDir=$HOME/fonts/microsoft -microsoftDir=$HOME/fonts/microsoft \
-notoDir=$HOME/fonts/noto
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 etc go test golang.org/x/image/font/sfnt -test.run=ProprietaryMicrosoft -args -proprietary etc
*/ */
// 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?
import ( import (
@ -88,6 +87,13 @@ var (
"/usr/share/fonts/truetype/msttcorefonts", "/usr/share/fonts/truetype/msttcorefonts",
"directory name for the Microsoft proprietary fonts", "directory name for the Microsoft proprietary fonts",
) )
notoDir = flag.String(
"notoDir",
// Get the fonts from https://www.google.com/get/noto/
"",
"directory name for the Noto proprietary fonts",
)
) )
func TestProprietaryAdobeSourceCodeProOTF(t *testing.T) { func TestProprietaryAdobeSourceCodeProOTF(t *testing.T) {
@ -186,6 +192,14 @@ func TestProprietaryMicrosoftWebdings(t *testing.T) {
testProprietary(t, "microsoft", "Webdings.ttf", 200, -1) testProprietary(t, "microsoft", "Webdings.ttf", 200, -1)
} }
func TestProprietaryNotoColorEmoji(t *testing.T) {
testProprietary(t, "noto", "NotoColorEmoji.ttf", 2300, -1)
}
func TestProprietaryNotoSansRegular(t *testing.T) {
testProprietary(t, "noto", "NotoSans-Regular.ttf", 2400, -1)
}
// testProprietary tests that we can load every glyph in the named font. // testProprietary tests that we can load every glyph in the named font.
// //
// The exact number of glyphs in the font can differ across its various // The exact number of glyphs in the font can differ across its various
@ -219,6 +233,8 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi
dir = *dejavuDir dir = *dejavuDir
case "microsoft": case "microsoft":
dir = *microsoftDir dir = *microsoftDir
case "noto":
dir = *notoDir
default: default:
panic("unreachable") panic("unreachable")
} }
@ -287,7 +303,7 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi
iMax = firstUnsupportedGlyph iMax = firstUnsupportedGlyph
} }
for i, numErrors := 0, 0; i < iMax; i++ { for i, numErrors := 0, 0; i < iMax; i++ {
if _, err := f.LoadGlyph(&buf, GlyphIndex(i), ppem, nil); err != nil { if _, err := f.LoadGlyph(&buf, GlyphIndex(i), ppem, nil); err != nil && err != ErrColoredGlyph {
t.Errorf("LoadGlyph(%d): %v", i, err) t.Errorf("LoadGlyph(%d): %v", i, err)
numErrors++ numErrors++
} }
@ -409,6 +425,9 @@ var proprietaryVersions = map[string]string{
"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",
"noto/NotoColorEmoji.ttf": "Version 1.33",
"noto/NotoSans-Regular.ttf": "Version 1.06",
} }
// proprietaryFullNames holds the expected full name of each proprietary font // proprietaryFullNames holds the expected full name of each proprietary font
@ -441,6 +460,9 @@ var proprietaryFullNames = map[string]string{
"microsoft/Comic_Sans_MS.ttf": "Comic Sans MS", "microsoft/Comic_Sans_MS.ttf": "Comic Sans MS",
"microsoft/Times_New_Roman.ttf": "Times New Roman", "microsoft/Times_New_Roman.ttf": "Times New Roman",
"microsoft/Webdings.ttf": "Webdings", "microsoft/Webdings.ttf": "Webdings",
"noto/NotoColorEmoji.ttf": "Noto Color Emoji",
"noto/NotoSans-Regular.ttf": "Noto Sans",
} }
// proprietaryGlyphIndexTestCases hold a sample of each font's rune to glyph // proprietaryGlyphIndexTestCases hold a sample of each font's rune to glyph
@ -1152,6 +1174,27 @@ var proprietaryGlyphTestCases = map[string]map[rune][]Segment{
transform(-1<<14, 0, 0, +1<<14, 653, 0, quadTo(482, -217, 560, -384)), transform(-1<<14, 0, 0, +1<<14, 653, 0, quadTo(482, -217, 560, -384)),
}, },
}, },
"noto/NotoSans-Regular.ttf": {
'i': {
// - contour #0
moveTo(354, 0),
lineTo(174, 0),
lineTo(174, 1098),
lineTo(354, 1098),
lineTo(354, 0),
// - contour #1
moveTo(160, 1395),
quadTo(160, 1455, 190, 1482),
quadTo(221, 1509, 266, 1509),
quadTo(308, 1509, 339, 1482),
quadTo(371, 1455, 371, 1395),
quadTo(371, 1336, 339, 1308),
quadTo(308, 1280, 266, 1280),
quadTo(221, 1280, 190, 1308),
quadTo(160, 1336, 160, 1395),
},
},
} }
type kernTestCase struct { type kernTestCase struct {

View File

@ -64,6 +64,9 @@ const (
) )
var ( var (
// ErrColoredGlyph indicates that the requested glyph is not a monochrome
// vector glyph, such as a colored (bitmap or vector) emoji glyph.
ErrColoredGlyph = errors.New("sfnt: colored glyph")
// 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")
@ -543,6 +546,12 @@ type Font struct {
// TODO: cff2, vorg? // TODO: cff2, vorg?
cff table cff table
// https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Tables Related to Bitmap Glyphs".
//
// TODO: Others?
cblc table
// https://www.microsoft.com/typography/otspec/otff.htm#otttables // https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Advanced Typographic Tables". // "Advanced Typographic Tables".
// //
@ -559,6 +568,7 @@ type Font struct {
glyphIndex glyphIndexFunc glyphIndex glyphIndexFunc
bounds [4]int16 bounds [4]int16
indexToLocFormat bool // false means short, true means long. indexToLocFormat bool // false means short, true means long.
isColorBitmap bool
isPostScript bool isPostScript bool
kernNumPairs int32 kernNumPairs int32
kernOffset int32 kernOffset int32
@ -599,7 +609,7 @@ func (f *Font) initialize(offset int, isDfont bool) error {
if err != nil { if err != nil {
return err return err
} }
buf, glyphData, err := f.parseGlyphData(buf, numGlyphs, indexToLocFormat, isPostScript) buf, glyphData, isColorBitmap, err := f.parseGlyphData(buf, numGlyphs, indexToLocFormat, isPostScript)
if err != nil { if err != nil {
return err return err
} }
@ -628,6 +638,7 @@ func (f *Font) initialize(offset int, isDfont bool) error {
f.cached.glyphIndex = glyphIndex f.cached.glyphIndex = glyphIndex
f.cached.bounds = bounds f.cached.bounds = bounds
f.cached.indexToLocFormat = indexToLocFormat f.cached.indexToLocFormat = indexToLocFormat
f.cached.isColorBitmap = isColorBitmap
f.cached.isPostScript = isPostScript f.cached.isPostScript = isPostScript
f.cached.kernNumPairs = kernNumPairs f.cached.kernNumPairs = kernNumPairs
f.cached.kernOffset = kernOffset f.cached.kernOffset = kernOffset
@ -701,6 +712,8 @@ func (f *Font) initializeTables(offset int, isDfont bool) (buf1 []byte, isPostSc
// Match the 4-byte tag as a uint32. For example, "OS/2" is 0x4f532f32. // Match the 4-byte tag as a uint32. For example, "OS/2" is 0x4f532f32.
switch tag { switch tag {
case 0x43424c43:
f.cblc = table{o, n}
case 0x43464620: case 0x43464620:
f.cff = table{o, n} f.cff = table{o, n}
case 0x4f532f32: case 0x4f532f32:
@ -983,7 +996,7 @@ type glyphData struct {
fdSelect fdSelect fdSelect fdSelect
} }
func (f *Font) parseGlyphData(buf []byte, numGlyphs int32, indexToLocFormat, isPostScript bool) (buf1 []byte, ret glyphData, err error) { func (f *Font) parseGlyphData(buf []byte, numGlyphs int32, indexToLocFormat, isPostScript bool) (buf1 []byte, ret glyphData, isColorBitmap bool, err error) {
if isPostScript { if isPostScript {
p := cffParser{ p := cffParser{
src: &f.src, src: &f.src,
@ -993,19 +1006,25 @@ func (f *Font) parseGlyphData(buf []byte, numGlyphs int32, indexToLocFormat, isP
} }
ret, err = p.parse(numGlyphs) ret, err = p.parse(numGlyphs)
if err != nil { if err != nil {
return nil, glyphData{}, err return nil, glyphData{}, false, err
} }
} else { } else if f.loca.length != 0 {
ret.locations, err = parseLoca(&f.src, f.loca, f.glyf.offset, indexToLocFormat, numGlyphs) ret.locations, err = parseLoca(&f.src, f.loca, f.glyf.offset, indexToLocFormat, numGlyphs)
if err != nil { if err != nil {
return nil, glyphData{}, err return nil, glyphData{}, false, err
} }
} } else if f.cblc.length != 0 {
if len(ret.locations) != int(numGlyphs+1) { isColorBitmap = true
return nil, glyphData{}, errInvalidLocationData // TODO: parse the CBLC (and CBDT) tables. For now, we return a font
// with empty glyphs.
ret.locations = make([]uint32, numGlyphs+1)
} }
return buf, ret, nil if len(ret.locations) != int(numGlyphs+1) {
return nil, glyphData{}, false, errInvalidLocationData
}
return buf, ret, isColorBitmap, nil
} }
func (f *Font) parsePost(buf []byte, numGlyphs int32) (buf1 []byte, postTableVersion uint32, err error) { func (f *Font) parsePost(buf []byte, numGlyphs int32) (buf1 []byte, postTableVersion uint32, err error) {
@ -1105,13 +1124,18 @@ type LoadGlyphOptions struct {
// //
// In the returned Segments' (x, y) coordinates, the Y axis increases down. // In the returned Segments' (x, y) coordinates, the Y axis increases down.
// //
// It returns ErrNotFound if the glyph index is out of range. // It returns ErrNotFound if the glyph index is out of range. It returns
// ErrColoredGlyph if the glyph is not a monochrome vector glyph, such as a
// colored (bitmap or vector) emoji glyph.
func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, opts *LoadGlyphOptions) ([]Segment, error) { func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, opts *LoadGlyphOptions) ([]Segment, error) {
if b == nil { if b == nil {
b = &Buffer{} b = &Buffer{}
} }
b.segments = b.segments[:0] b.segments = b.segments[:0]
if f.cached.isColorBitmap {
return nil, ErrColoredGlyph
}
if f.cached.isPostScript { if f.cached.isPostScript {
buf, offset, length, err := f.viewGlyphData(b, x) buf, offset, length, err := f.viewGlyphData(b, x)
if err != nil { if err != nil {
@ -1124,11 +1148,9 @@ func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, opts *Load
if !b.psi.type2Charstrings.ended { if !b.psi.type2Charstrings.ended {
return nil, errInvalidCFFTable return nil, errInvalidCFFTable
} }
} else { } else if err := loadGlyf(f, b, x, 0, 0); err != nil {
if err := loadGlyf(f, b, x, 0, 0); err != nil {
return nil, err return nil, err
} }
}
// Scale the segments. If we want to support hinting, we'll have to push // Scale the segments. If we want to support hinting, we'll have to push
// the scaling computation into the PostScript / TrueType specific glyph // the scaling computation into the PostScript / TrueType specific glyph