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