From d835a09709a473a54d43dfc3cd0495bf49801ade Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Sat, 20 May 2017 18:22:12 +1000 Subject: [PATCH] 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 --- font/sfnt/proprietary_test.go | 51 ++++++++++++++++++++++++++++++++--- font/sfnt/sfnt.go | 50 ++++++++++++++++++++++++---------- 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/font/sfnt/proprietary_test.go b/font/sfnt/proprietary_test.go index dcafdd4..d98961b 100644 --- a/font/sfnt/proprietary_test.go +++ b/font/sfnt/proprietary_test.go @@ -23,15 +23,14 @@ go test golang.org/x/image/font/sfnt -args -proprietary \ -adobeDir=$HOME/fonts/adobe \ -appleDir=$HOME/fonts/apple \ -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: 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? import ( @@ -88,6 +87,13 @@ var ( "/usr/share/fonts/truetype/msttcorefonts", "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) { @@ -186,6 +192,14 @@ func TestProprietaryMicrosoftWebdings(t *testing.T) { 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. // // 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 case "microsoft": dir = *microsoftDir + case "noto": + dir = *notoDir default: panic("unreachable") } @@ -287,7 +303,7 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi iMax = firstUnsupportedGlyph } 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) numErrors++ } @@ -409,6 +425,9 @@ var proprietaryVersions = map[string]string{ "microsoft/Comic_Sans_MS.ttf": "Version 2.10", "microsoft/Times_New_Roman.ttf": "Version 2.82", "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 @@ -441,6 +460,9 @@ var proprietaryFullNames = map[string]string{ "microsoft/Comic_Sans_MS.ttf": "Comic Sans MS", "microsoft/Times_New_Roman.ttf": "Times New Roman", "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 @@ -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)), }, }, + + "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 { diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go index 34587da..53ac94b 100644 --- a/font/sfnt/sfnt.go +++ b/font/sfnt/sfnt.go @@ -64,6 +64,9 @@ const ( ) 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 = errors.New("sfnt: not found") @@ -543,6 +546,12 @@ type Font struct { // TODO: cff2, vorg? 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 // "Advanced Typographic Tables". // @@ -559,6 +568,7 @@ type Font struct { glyphIndex glyphIndexFunc bounds [4]int16 indexToLocFormat bool // false means short, true means long. + isColorBitmap bool isPostScript bool kernNumPairs int32 kernOffset int32 @@ -599,7 +609,7 @@ func (f *Font) initialize(offset int, isDfont bool) error { if err != nil { return err } - buf, glyphData, err := f.parseGlyphData(buf, numGlyphs, indexToLocFormat, isPostScript) + buf, glyphData, isColorBitmap, err := f.parseGlyphData(buf, numGlyphs, indexToLocFormat, isPostScript) if err != nil { return err } @@ -628,6 +638,7 @@ func (f *Font) initialize(offset int, isDfont bool) error { f.cached.glyphIndex = glyphIndex f.cached.bounds = bounds f.cached.indexToLocFormat = indexToLocFormat + f.cached.isColorBitmap = isColorBitmap f.cached.isPostScript = isPostScript f.cached.kernNumPairs = kernNumPairs 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. switch tag { + case 0x43424c43: + f.cblc = table{o, n} case 0x43464620: f.cff = table{o, n} case 0x4f532f32: @@ -983,7 +996,7 @@ type glyphData struct { 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 { p := cffParser{ src: &f.src, @@ -993,19 +1006,25 @@ func (f *Font) parseGlyphData(buf []byte, numGlyphs int32, indexToLocFormat, isP } ret, err = p.parse(numGlyphs) 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) if err != nil { - return nil, glyphData{}, err + return nil, glyphData{}, false, err } - } - if len(ret.locations) != int(numGlyphs+1) { - return nil, glyphData{}, errInvalidLocationData + } else if f.cblc.length != 0 { + isColorBitmap = true + // 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) { @@ -1105,13 +1124,18 @@ type LoadGlyphOptions struct { // // 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) { if b == nil { b = &Buffer{} } b.segments = b.segments[:0] + if f.cached.isColorBitmap { + return nil, ErrColoredGlyph + } if f.cached.isPostScript { buf, offset, length, err := f.viewGlyphData(b, x) if err != nil { @@ -1124,10 +1148,8 @@ func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, opts *Load if !b.psi.type2Charstrings.ended { return nil, errInvalidCFFTable } - } else { - if err := loadGlyf(f, b, x, 0, 0); err != nil { - return nil, err - } + } else if err := loadGlyf(f, b, x, 0, 0); err != nil { + return nil, err } // Scale the segments. If we want to support hinting, we'll have to push