diff --git a/font/sfnt/gpos.go b/font/sfnt/gpos.go new file mode 100644 index 0000000..f136877 --- /dev/null +++ b/font/sfnt/gpos.go @@ -0,0 +1,544 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sfnt + +import ( + "sort" +) + +const ( + hexScriptLatn = uint32(0x6c61746e) // latn + hexScriptDFLT = uint32(0x44464c54) // DFLT + hexFeatureKern = uint32(0x6b65726e) // kern +) + +//kernFunc returns the unscaled kerning value for kerning pair a+b. +// Returns ErrNotFound if no kerning is specified for this pair. +type kernFunc func(a, b GlyphIndex) (int16, error) + +func (f *Font) parseGPOSKern(buf []byte) ([]byte, []kernFunc, error) { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos + + if f.gpos.length == 0 { + return buf, nil, nil + } + const headerSize = 10 // GPOS header v1.1 is 14 bytes, but we don't support FeatureVariations + if f.gpos.length < headerSize { + return buf, nil, errInvalidGPOSTable + } + + buf, err := f.src.view(buf, int(f.gpos.offset), headerSize) + if err != nil { + return buf, nil, err + } + + // check for version 1.0/1.1 + if u16(buf) != 1 || u16(buf[2:]) > 1 { + return buf, nil, errUnsupportedGPOSTable + } + scriptListOffset := u16(buf[4:]) + featureListOffset := u16(buf[6:]) + lookupListOffset := u16(buf[8:]) + + // get all feature indices for latn script + buf, featureIdxs, err := f.parseGPOSScriptFeatures(buf, int(f.gpos.offset)+int(scriptListOffset), hexScriptLatn) + if err != nil { + return buf, nil, err + } + if len(featureIdxs) == 0 { + // get all feature indices for DFLT script + buf, featureIdxs, err = f.parseGPOSScriptFeatures(buf, int(f.gpos.offset)+int(scriptListOffset), hexScriptDFLT) + if err != nil { + return buf, nil, err + } + if len(featureIdxs) == 0 { + return buf, nil, nil + } + } + + // get all lookup indices for kern features + buf, lookupIdx, err := f.parseGPOSFeaturesLookup(buf, int(f.gpos.offset)+int(featureListOffset), featureIdxs, hexFeatureKern) + + // LookupTableList: lookupCount,[]lookups + buf, numLookupTables, err := f.src.varLenView(buf, int(f.gpos.offset)+int(lookupListOffset), 2, 0, 2) + if err != nil { + return buf, nil, err + } + + var kernFuncs []kernFunc + +lookupTables: + for _, n := range lookupIdx { + if n > numLookupTables { + return buf, nil, errInvalidGPOSTable + } + tableOffset := int(f.gpos.offset) + int(lookupListOffset) + int(u16(buf[2+n*2:])) + + // LookupTable: lookupType, lookupFlag, subTableCount, []subtableOffsets, markFilteringSet + buf, numSubTables, err := f.src.varLenView(buf, tableOffset, 8, 4, 2) + if err != nil { + return buf, nil, err + } + + flags := u16(buf[2:]) + + subTableOffsets := make([]int, numSubTables) + for i := 0; i < int(numSubTables); i++ { + subTableOffsets[i] = int(tableOffset) + int(u16(buf[6+i*2:])) + } + + switch lookupType := u16(buf); lookupType { + case 2: // PairPos table + case 9: + // Extension Positioning table defines an additional u32 offset + // to allow subtables to exceed the 16-bit limit. + for i := range subTableOffsets { + buf, err = f.src.view(buf, subTableOffsets[i], 8) + if err != nil { + return buf, nil, err + } + if format := u16(buf); format != 1 { + return buf, nil, errUnsupportedExtensionPosFormat + } + if lookupType := u16(buf[2:]); lookupType != 2 { + continue lookupTables + } + subTableOffsets[i] += int(u32(buf[4:])) + } + default: // other types are not supported + continue + } + + if flags&0x0010 > 0 { + // useMarkFilteringSet enabled, skip as it is not supported + continue + } + + for _, subTableOffset := range subTableOffsets { + buf, err = f.src.view(buf, int(subTableOffset), 4) + if err != nil { + return buf, nil, err + } + format := u16(buf) + + var lookupIndex indexLookupFunc + buf, lookupIndex, err = f.makeCachedCoverageLookup(buf, subTableOffset+int(u16(buf[2:]))) + if err != nil { + return buf, nil, err + } + + switch format { + case 1: // Adjustments for Glyph Pairs + buf, kern, err := f.parsePairPosFormat1(buf, subTableOffset, lookupIndex) + if err != nil { + return buf, nil, err + } + if kern != nil { + kernFuncs = append(kernFuncs, kern) + } + case 2: // Class Pair Adjustment + buf, kern, err := f.parsePairPosFormat2(buf, subTableOffset, lookupIndex) + if err != nil { + return buf, nil, err + } + if kern != nil { + kernFuncs = append(kernFuncs, kern) + } + } + } + } + + return buf, kernFuncs, nil +} + +func (f *Font) parsePairPosFormat1(buf []byte, offset int, lookupIndex indexLookupFunc) ([]byte, kernFunc, error) { + // PairPos Format 1: posFormat, coverageOffset, valueFormat1, + // valueFormat2, pairSetCount, []pairSetOffsets + var err error + var nPairs int + buf, nPairs, err = f.src.varLenView(buf, offset, 10, 8, 2) + if err != nil { + return buf, nil, err + } + // check valueFormat1 and valueFormat2 flags + if u16(buf[4:]) != 0x04 || u16(buf[6:]) != 0x00 { + // we only support kerning with X_ADVANCE for first glyph + return buf, nil, nil + } + + // PairPos table contains an array of offsets to PairSet + // tables, which contains an array of PairValueRecords. + // Calculate length of complete PairPos table by jumping to + // last PairSet. + // We need to iterate all offsets to find the last pair as + // offsets are not sorted and can be repeated. + var lastPairSetOffset int + for n := 0; n < nPairs; n++ { + pairOffset := int(u16(buf[10+n*2:])) + if pairOffset > lastPairSetOffset { + lastPairSetOffset = pairOffset + } + } + buf, err = f.src.view(buf, offset+lastPairSetOffset, 2) + if err != nil { + return buf, nil, err + } + + pairValueCount := int(u16(buf)) + // Each PairSet contains the secondGlyph (u16) and one or more value records (all u16). + // We only support lookup tables with one value record (X_ADVANCE, see valueFormat1/2 above). + lastPairSetLength := 2 + pairValueCount*4 + + length := lastPairSetOffset + lastPairSetLength + buf, err = f.src.view(buf, offset, length) + if err != nil { + return buf, nil, err + } + + kern := makeCachedPairPosGlyph(lookupIndex, nPairs, buf) + return buf, kern, nil +} + +func (f *Font) parsePairPosFormat2(buf []byte, offset int, lookupIndex indexLookupFunc) ([]byte, kernFunc, error) { + // PairPos Format 2: + // posFormat, coverageOffset, valueFormat1, valueFormat2, + // classDef1Offset, classDef2Offset, class1Count, class2Count, + // []class1Records + var err error + buf, err = f.src.view(buf, offset, 16) + if err != nil { + return buf, nil, err + } + // check valueFormat1 and valueFormat2 flags + if u16(buf[4:]) != 0x04 || u16(buf[6:]) != 0x00 { + // we only support kerning with X_ADVANCE for first glyph + return buf, nil, nil + } + numClass1 := int(u16(buf[12:])) + numClass2 := int(u16(buf[14:])) + cdef1Offset := offset + int(u16(buf[8:])) + cdef2Offset := offset + int(u16(buf[10:])) + var cdef1, cdef2 classLookupFunc + buf, cdef1, err = f.makeCachedClassLookup(buf, cdef1Offset) + if err != nil { + return buf, nil, err + } + buf, cdef2, err = f.makeCachedClassLookup(buf, cdef2Offset) + if err != nil { + return buf, nil, err + } + + buf, err = f.src.view(buf, offset+16, numClass1*numClass2*2) + if err != nil { + return buf, nil, err + } + kern := makeCachedPairPosClass( + lookupIndex, + numClass1, + numClass2, + cdef1, + cdef2, + buf, + ) + + return buf, kern, nil +} + +// parseGPOSScriptFeatures returns all indices of features in FeatureTable that +// are valid for the given script. +// Returns features from DefaultLangSys, different languages are not supported. +// However, all observed fonts either do not use different languages or use the +// same features as DefaultLangSys. +func (f *Font) parseGPOSScriptFeatures(buf []byte, offset int, script uint32) ([]byte, []int, error) { + // ScriptList table: scriptCount, []scriptRecords{scriptTag, scriptOffset} + buf, numScriptTables, err := f.src.varLenView(buf, offset, 2, 0, 6) + if err != nil { + return buf, nil, err + } + + // Search ScriptTables for script + var scriptTableOffset uint16 + for i := 0; i < numScriptTables; i++ { + scriptTag := u32(buf[2+i*6:]) + if scriptTag == script { + scriptTableOffset = u16(buf[2+i*6+4:]) + break + } + } + if scriptTableOffset == 0 { + return buf, nil, nil + } + + // Script table: defaultLangSys, langSysCount, []langSysRecords{langSysTag, langSysOffset} + buf, err = f.src.view(buf, offset+int(scriptTableOffset), 2) + if err != nil { + return buf, nil, err + } + defaultLangSysOffset := u16(buf) + + if defaultLangSysOffset == 0 { + return buf, nil, nil + } + + // LangSys table: lookupOrder (reserved), requiredFeatureIndex, featureIndexCount, []featureIndices + buf, numFeatures, err := f.src.varLenView(buf, offset+int(scriptTableOffset)+int(defaultLangSysOffset), 6, 4, 2) + + featureIdxs := make([]int, numFeatures) + for i := range featureIdxs { + featureIdxs[i] = int(u16(buf[6+i*2:])) + } + return buf, featureIdxs, nil +} + +func (f *Font) parseGPOSFeaturesLookup(buf []byte, offset int, featureIdxs []int, feature uint32) ([]byte, []int, error) { + // FeatureList table: featureCount, []featureRecords{featureTag, featureOffset} + buf, numFeatureTables, err := f.src.varLenView(buf, offset, 2, 0, 6) + if err != nil { + return buf, nil, err + } + + lookupIdx := make([]int, 0, 4) + + for _, fidx := range featureIdxs { + if fidx > numFeatureTables { + return buf, nil, errInvalidGPOSTable + } + featureTag := u32(buf[2+fidx*6:]) + if featureTag != feature { + continue + } + featureOffset := u16(buf[2+fidx*6+4:]) + + buf, numLookups, err := f.src.varLenView(nil, offset+int(featureOffset), 4, 2, 2) + if err != nil { + return buf, nil, err + } + + for i := 0; i < numLookups; i++ { + lookupIdx = append(lookupIdx, int(u16(buf[4+i*2:]))) + } + } + + return buf, lookupIdx, nil +} + +func makeCachedPairPosGlyph(cov indexLookupFunc, num int, buf []byte) kernFunc { + glyphs := make([]byte, len(buf)) + copy(glyphs, buf) + return func(a, b GlyphIndex) (int16, error) { + idx, found := cov(a) + if !found { + return 0, ErrNotFound + } + if idx >= num { + return 0, ErrNotFound + } + offset := int(u16(glyphs[10+idx*2:])) + if offset+1 >= len(glyphs) { + return 0, errInvalidGPOSTable + } + + count := int(u16(glyphs[offset:])) + for i := 0; i < count; i++ { + secondGlyphIndex := GlyphIndex(int(u16(glyphs[offset+2+i*4:]))) + if secondGlyphIndex == b { + return int16(u16(glyphs[offset+2+i*4+2:])), nil + } + if secondGlyphIndex > b { + return 0, ErrNotFound + } + } + + return 0, ErrNotFound + } +} + +func makeCachedPairPosClass(cov indexLookupFunc, num1, num2 int, cdef1, cdef2 classLookupFunc, buf []byte) kernFunc { + glyphs := make([]byte, len(buf)) + copy(glyphs, buf) + return func(a, b GlyphIndex) (int16, error) { + // check coverage to avoid selection of default class 0 + _, found := cov(a) + if !found { + return 0, ErrNotFound + } + idxa := cdef1(a) + idxb := cdef2(b) + return int16(u16(glyphs[(idxb+idxa*num2)*2:])), nil + } +} + +// indexLookupFunc returns the index into a PairPos table for the provided glyph. +// Returns false if the glyph is not covered by this lookup. +type indexLookupFunc func(GlyphIndex) (int, bool) + +func (f *Font) makeCachedCoverageLookup(buf []byte, offset int) ([]byte, indexLookupFunc, error) { + var err error + buf, err = f.src.view(buf, offset, 2) + if err != nil { + return buf, nil, err + } + switch u16(buf) { + case 1: + // Coverage Format 1: coverageFormat, glyphCount, []glyphArray + buf, _, err = f.src.varLenView(buf, offset, 4, 2, 2) + if err != nil { + return buf, nil, err + } + return buf, makeCachedCoverageList(buf[2:]), nil + case 2: + // Coverage Format 2: coverageFormat, rangeCount, []rangeRecords{startGlyphID, endGlyphID, startCoverageIndex} + buf, _, err = f.src.varLenView(buf, offset, 4, 2, 6) + if err != nil { + return buf, nil, err + } + return buf, makeCachedCoverageRange(buf[2:]), nil + default: + return buf, nil, errUnsupportedCoverageFormat + } +} + +func makeCachedCoverageList(buf []byte) indexLookupFunc { + num := int(u16(buf)) + list := make([]byte, len(buf)-2) + copy(list, buf[2:]) + return func(gi GlyphIndex) (int, bool) { + idx := sort.Search(num, func(i int) bool { + return gi <= GlyphIndex(u16(list[i*2:])) + }) + if idx < num && GlyphIndex(u16(list[idx*2:])) == gi { + return idx, true + } + + return 0, false + } +} + +func makeCachedCoverageRange(buf []byte) indexLookupFunc { + num := int(u16(buf)) + ranges := make([]byte, len(buf)-2) + copy(ranges, buf[2:]) + return func(gi GlyphIndex) (int, bool) { + if num == 0 { + return 0, false + } + + // ranges is an array of startGlyphID, endGlyphID and startCoverageIndex + // Ranges are non-overlapping. + // The following GlyphIDs/index pairs are stored as follows: + // pairs: 130=0, 131=1, 132=2, 133=3, 134=4, 135=5, 137=6 + // ranges: 130, 135, 0 137, 137, 6 + // startCoverageIndex is used to calculate the index without counting + // the length of the preceeding ranges + + idx := sort.Search(num, func(i int) bool { + return gi <= GlyphIndex(u16(ranges[i*6:])) + }) + // idx either points to a matching start, or to the next range (or idx==num) + // e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range + + // check if gi is the start of a range, but only if sort.Search returned a valid result + if idx < num { + if start := u16(ranges[idx*6:]); gi == GlyphIndex(start) { + return int(u16(ranges[idx*6+4:])), true + } + } + // check if gi is in previous range + if idx > 0 { + idx-- + start, end := u16(ranges[idx*6:]), u16(ranges[idx*6+2:]) + if gi >= GlyphIndex(start) && gi <= GlyphIndex(end) { + return int(u16(ranges[idx*6+4:]) + uint16(gi) - start), true + } + } + + return 0, false + } +} + +// classLookupFunc returns the class ID for the provided glyph. Returns 0 +// (default class) for glyphs not covered by this lookup. +type classLookupFunc func(GlyphIndex) int + +func (f *Font) makeCachedClassLookup(buf []byte, offset int) ([]byte, classLookupFunc, error) { + var err error + buf, err = f.src.view(buf, offset, 2) + if err != nil { + return buf, nil, err + } + switch u16(buf) { + case 1: + // ClassDefFormat 1: classFormat, startGlyphID, glyphCount, []classValueArray + buf, _, err = f.src.varLenView(buf, offset, 6, 4, 2) + if err != nil { + return buf, nil, err + } + return buf, makeCachedClassLookupFormat1(buf), nil + case 2: + // ClassDefFormat 2: classFormat, classRangeCount, []classRangeRecords + buf, _, err = f.src.varLenView(buf, offset, 4, 2, 6) + if err != nil { + return buf, nil, err + } + return buf, makeCachedClassLookupFormat2(buf), nil + default: + return buf, nil, errUnsupportedClassDefFormat + } +} + +func makeCachedClassLookupFormat1(buf []byte) classLookupFunc { + startGI := u16(buf[2:]) + num := u16(buf[4:]) + classIDs := make([]byte, len(buf)-4) + copy(classIDs, buf[6:]) + + return func(gi GlyphIndex) int { + // classIDs is an array of target class IDs. gi is the index into that array (minus startGI). + if gi < GlyphIndex(startGI) || gi >= GlyphIndex(startGI+num) { + // default to class 0 + return 0 + } + return int(u16(classIDs[(int(gi)-int(startGI))*2:])) + } +} + +func makeCachedClassLookupFormat2(buf []byte) classLookupFunc { + num := int(u16(buf[2:])) + classRanges := make([]byte, len(buf)-2) + copy(classRanges, buf[4:]) + + return func(gi GlyphIndex) int { + if num == 0 { + return 0 // default to class 0 + } + + // classRange is an array of startGlyphID, endGlyphID and target class ID. + // Ranges are non-overlapping. + // E.g. 130, 135, 1 137, 137, 5 etc + + idx := sort.Search(num, func(i int) bool { + return gi <= GlyphIndex(u16(classRanges[i*6:])) + }) + // idx either points to a matching start, or to the next range (or idx==num) + // e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range + + // check if gi is the start of a range, but only if sort.Search returned a valid result + if idx < num { + if start := u16(classRanges[idx*6:]); gi == GlyphIndex(start) { + return int(u16(classRanges[idx*6+4:])) + } + } + // check if gi is in previous range + if idx > 0 { + idx-- + start, end := u16(classRanges[idx*6:]), u16(classRanges[idx*6+2:]) + if gi >= GlyphIndex(start) && gi <= GlyphIndex(end) { + return int(u16(classRanges[idx*6+4:])) + } + } + // default to class 0 + return 0 + } +} diff --git a/font/sfnt/kern_test.go b/font/sfnt/kern_test.go new file mode 100644 index 0000000..163dac7 --- /dev/null +++ b/font/sfnt/kern_test.go @@ -0,0 +1,141 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sfnt + +/* +This file contains opt-in tests for kerning in user provided fonts. + +Kerning information in kern and GPOS tables can be quite complex. These tests +recursively load all fonts from -bulkFontDirs and try to kern all possible +glyph pairs. + +These tests only check if there are no errors during kerning. Tests of actual +kerning values are in proprietary_test.go. + +Note: CJK fonts can contain billions of posible kerning pairs. Testing for +these fonts stops after -bulkMaxKernPairs. + +To opt-in: + +go test golang.org/x/image/font/sfnt -test.run=BulkKern -args -bulk -bulkFontDirs /Library/Fonts:./myfonts +*/ + +import ( + "flag" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "testing" + + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" +) + +var ( + bulk = flag.Bool("bulk", false, "") + + fontDirs = flag.String( + "bulkFontDirs", + "./", + "separated directories to search for fonts", + ) + maxKernPairs = flag.Int( + "bulkMaxKernPairs", + 20000000, + "skip testing of kerning after this many tested pairs", + ) +) + +func TestBulkKern(t *testing.T) { + if !*bulk { + t.Skip("skipping bulk font test") + } + + for _, fontDir := range filepath.SplitList(*fontDirs) { + err := filepath.Walk(fontDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if strings.HasSuffix(path, ".ttf") || strings.HasSuffix(path, ".otf") { + t.Run(info.Name(), testFontKerning(filepath.Join(path))) + } + return nil + }) + if err != nil { + t.Fatal("error finding fonts", err) + } + } + +} + +func testFontKerning(fname string) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + b, err := ioutil.ReadFile(fname) + if err != nil { + t.Fatal(err) + } + fnt, err := Parse(b) + if err != nil { + t.Fatal(err) + } + + buf := &Buffer{} + + // collect all GlyphIndex + glyphs := make([]GlyphIndex, 1, fnt.NumGlyphs()) + glyphs[0] = GlyphIndex(0) + r := rune(0) + for r < 0xffff { + g, err := fnt.GlyphIndex(buf, r) + r++ + if g == 0 || err == ErrNotFound { + continue + } + if err != nil { + t.Fatal(err) + } + glyphs = append(glyphs, g) + if len(glyphs) == fnt.NumGlyphs() { + break + } + } + + var kerned, tested int + for _, g1 := range glyphs { + for _, g2 := range glyphs { + if tested >= *maxKernPairs { + log.Printf("stop testing after %d or %d kerning pairs (found %d pairs)", + tested, len(glyphs)*len(glyphs), kerned) + return + } + + tested++ + adv, err := fnt.Kern(buf, g1, g2, fixed.I(20), font.HintingNone) + if err == ErrNotFound { + continue + } + if err != nil { + t.Fatal(err) + } + if adv != 0 { + kerned++ + } + } + } + + log.Printf("found %d kerning pairs for %d glyphs (%.1f%%) in %q", + kerned, + len(glyphs), + 100*float64(kerned)/float64(len(glyphs)*len(glyphs)), + fname, + ) + } +} diff --git a/font/sfnt/proprietary_test.go b/font/sfnt/proprietary_test.go index bb14a34..39c2cdd 100644 --- a/font/sfnt/proprietary_test.go +++ b/font/sfnt/proprietary_test.go @@ -168,6 +168,10 @@ func TestProprietaryAppleHiragino1(t *testing.T) { testProprietary(t, "apple", "ヒラギノ角ゴシック W0.ttc?1", 9000, -1) } +func TestProprietaryDejaVuSans(t *testing.T) { + testProprietary(t, "dejavu", "DejaVuSans.ttf", 3300, -1) +} + func TestProprietaryDejaVuSansExtraLight(t *testing.T) { testProprietary(t, "dejavu", "DejaVuSans-ExtraLight.ttf", 2000, -1) } @@ -427,6 +431,7 @@ var proprietaryVersions = map[string]string{ "apple/ヒラギノ角ゴシック W0.ttc?1": "11.0d7e1", "dejavu/DejaVuSans-ExtraLight.ttf": "Version 2.37", + "dejavu/DejaVuSans.ttf": "Version 2.37", "dejavu/DejaVuSansMono.ttf": "Version 2.37", "dejavu/DejaVuSerif.ttf": "Version 2.37", @@ -464,6 +469,7 @@ var proprietaryFullNames = map[string]string{ "apple/ヒラギノ角ゴシック W0.ttc?1": ".Hiragino Kaku Gothic Interface W0", "dejavu/DejaVuSans-ExtraLight.ttf": "DejaVu Sans ExtraLight", + "dejavu/DejaVuSans.ttf": "DejaVu Sans", "dejavu/DejaVuSansMono.ttf": "DejaVu Sans Mono", "dejavu/DejaVuSerif.ttf": "DejaVu Serif", @@ -1267,6 +1273,15 @@ type kernTestCase struct { // proprietaryKernTestCases hold a sample of each font's kerning pairs. The // numerical values can be verified by running the ttx tool. var proprietaryKernTestCases = map[string][]kernTestCase{ + "adobe/SourceSansPro-Regular.otf": { + {1000, font.HintingNone, [2]rune{'A', 'V'}, -14}, + {1000, font.HintingNone, [2]rune{'F', '\u0129'}, 36}, + }, + "adobe/SourceHanSansSC-Regular.otf": { + // U+3043 HIRAGANA LETTER SMALL I + // U+3067 HIRAGANA LETTER DE + {1000, font.HintingNone, [2]rune{'\u3043', '\u3067'}, -20}, + }, "dejavu/DejaVuSans-ExtraLight.ttf": { {2048, font.HintingNone, [2]rune{'A', 'A'}, 57}, {2048, font.HintingNone, [2]rune{'W', 'A'}, -112}, @@ -1274,10 +1289,16 @@ var proprietaryKernTestCases = map[string][]kernTestCase{ // U+01FA LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE // U+1E82 LATIN CAPITAL LETTER W WITH ACUTE {2048, font.HintingNone, [2]rune{'\u00c1', 'A'}, 57}, - // TODO: enable these next two test cases, when we support multiple - // kern subtables. - // {2048, font.HintingNone, [2]rune{'\u01fa', 'A'}, 57}, - // {2048, font.HintingNone, [2]rune{'\u1e82', 'A'}, -112}, + {2048, font.HintingNone, [2]rune{'\u01fa', 'A'}, 57}, + {2048, font.HintingNone, [2]rune{'\u1e82', 'A'}, -112}, + }, + "dejavu/DejaVuSans.ttf": { + // U+EF13 + U+EF19 are private use codes, but DejaVuSans has these + // codes in a rarely used ClassDef format 1 + {2048, font.HintingNone, [2]rune{'\uef13', '\uef19'}, -40}, + {2048, font.HintingNone, [2]rune{'\uef13', '\uef13'}, 0}, + // Use U+EF13 to trigger default class in ClassDef format 2 + {2048, font.HintingNone, [2]rune{'A', '\uef13'}, 0}, }, "microsoft/Arial.ttf": { {2048, font.HintingNone, [2]rune{'A', 'V'}, -152}, @@ -1309,6 +1330,35 @@ var proprietaryKernTestCases = map[string][]kernTestCase{ "microsoft/Webdings.ttf": { {2048, font.HintingNone, [2]rune{'\uf041', '\uf042'}, 0}, }, + "noto/NotoSans-Regular.ttf": { + {2048, font.HintingNone, [2]rune{'A', 'V'}, -40}, + {2048, font.HintingNone, [2]rune{'A', 'W'}, -40}, + {2048, font.HintingNone, [2]rune{'A', 'T'}, -70}, + {2048, font.HintingNone, [2]rune{'V', 'A'}, -40}, + {2048, font.HintingNone, [2]rune{'W', 'A'}, -40}, + {2048, font.HintingNone, [2]rune{'T', 'A'}, -70}, + // U+00C1 LATIN CAPITAL LETTER A WITH ACUTE + // U+01FA LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE + // U+1E82 LATIN CAPITAL LETTER W WITH ACUTE + {2048, font.HintingNone, [2]rune{'\u00c1', 'T'}, -70}, + {2048, font.HintingNone, [2]rune{'\u01fa', 'T'}, -70}, + {2048, font.HintingNone, [2]rune{'\u1e82', 'A'}, -40}, + + // NotoSans-Regular.ttf uses two PairPos format=1 tables (with ExtensionPos). + // Test first and last pairs in both tables. + // First pair in first table + {2048, font.HintingNone, [2]rune{'F', '?'}, 20}, + // U+04c3 CYRILLIC CAPITAL LETTER KA WITH HOOK + // U+0424 CYRILLIC CAPITAL LETTER EF + // Last pair in first table + {2048, font.HintingNone, [2]rune{'\u04c3', '\u0424'}, -20}, + // First pair in second table + {2048, font.HintingNone, [2]rune{'"', 'A'}, -70}, + // + // + // Last pair in second table + {2048, font.HintingNone, [2]rune{'\u1f4d', '\u03d2'}, -10}, + }, } // proprietaryFDSelectTestCases hold a sample of each font's Font Dict Select diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go index e7125bf..f7becca 100644 --- a/font/sfnt/sfnt.go +++ b/font/sfnt/sfnt.go @@ -77,6 +77,7 @@ var ( errInvalidDfont = errors.New("sfnt: invalid dfont") errInvalidFont = errors.New("sfnt: invalid font") errInvalidFontCollection = errors.New("sfnt: invalid font collection") + errInvalidGPOSTable = errors.New("sfnt: invalid GPOS table") errInvalidGlyphData = errors.New("sfnt: invalid glyph data") errInvalidGlyphDataLength = errors.New("sfnt: invalid glyph data length") errInvalidHeadTable = errors.New("sfnt: invalid head table") @@ -97,11 +98,14 @@ var ( errUnsupportedCFFFDSelectTable = errors.New("sfnt: unsupported CFF FDSelect table") errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version") + errUnsupportedClassDefFormat = errors.New("sfnt: unsupported class definition format") errUnsupportedCmapEncodings = errors.New("sfnt: unsupported cmap encodings") errUnsupportedCompoundGlyph = errors.New("sfnt: unsupported compound glyph") + errUnsupportedCoverageFormat = errors.New("sfnt: unsupported coverage format") + errUnsupportedExtensionPosFormat = errors.New("sfnt: unsupported extension positioning format") + errUnsupportedGPOSTable = errors.New("sfnt: unsupported GPOS table") errUnsupportedGlyphDataLength = errors.New("sfnt: unsupported glyph data length") errUnsupportedKernTable = errors.New("sfnt: unsupported kern table") - errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding") errUnsupportedNumberOfCmapSegments = errors.New("sfnt: unsupported number of cmap segments") errUnsupportedNumberOfFontDicts = errors.New("sfnt: unsupported number of font dicts") errUnsupportedNumberOfFonts = errors.New("sfnt: unsupported number of fonts") @@ -110,6 +114,7 @@ var ( errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables") errUnsupportedPlatformEncoding = errors.New("sfnt: unsupported platform encoding") errUnsupportedPostTable = errors.New("sfnt: unsupported post table") + errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding") errUnsupportedTableOffsetLength = errors.New("sfnt: unsupported table offset or length") errUnsupportedType2Charstring = errors.New("sfnt: unsupported Type 2 Charstring") ) @@ -244,6 +249,33 @@ func (s *source) view(buf []byte, offset, length int) ([]byte, error) { return buf, nil } +// varLenView returns bytes from the given offset for sub-tables with varying +// length. The length of bytes is determined by staticLength plus n*itemLength, +// where n is read as uint16 from countOffset (relative to offset). buf is an +// optional scratch buffer (see source.view()) +func (s *source) varLenView(buf []byte, offset, staticLength, countOffset, itemLength int) ([]byte, int, error) { + if 0 > offset || offset > offset+staticLength { + return nil, 0, errInvalidBounds + } + if 0 > countOffset || countOffset+1 >= staticLength { + return nil, 0, errInvalidBounds + } + + // read static part which contains our count + buf, err := s.view(buf, offset, staticLength) + if err != nil { + return nil, 0, err + } + + count := int(u16(buf[countOffset:])) + buf, err = s.view(buf, offset, staticLength+count*itemLength) + if err != nil { + return nil, 0, err + } + + return buf, count, nil +} + // u16 returns the uint16 in the table t at the relative offset i. // // buf is an optional scratch buffer as per the source.view method. @@ -557,7 +589,8 @@ type Font struct { // https://www.microsoft.com/typography/otspec/otff.htm#otttables // "Advanced Typographic Tables". // - // TODO: base, gdef, gpos, gsub, jstf, math? + // TODO: base, gdef, gsub, jstf, math? + gpos table // https://www.microsoft.com/typography/otspec/otff.htm#otttables // "Other OpenType Tables". @@ -577,6 +610,7 @@ type Font struct { isPostScript bool kernNumPairs int32 kernOffset int32 + kernFuncs []kernFunc lineGap int32 numHMetrics int32 post *PostTable @@ -629,6 +663,10 @@ func (f *Font) initialize(offset int, isDfont bool) error { if err != nil { return err } + buf, kernFuncs, err := f.parseGPOSKern(buf) + if err != nil { + return err + } buf, ascent, descent, lineGap, run, rise, numHMetrics, err := f.parseHhea(buf, numGlyphs) if err != nil { return err @@ -657,6 +695,7 @@ func (f *Font) initialize(offset int, isDfont bool) error { f.cached.isPostScript = isPostScript f.cached.kernNumPairs = kernNumPairs f.cached.kernOffset = kernOffset + f.cached.kernFuncs = kernFuncs f.cached.lineGap = lineGap f.cached.numHMetrics = numHMetrics f.cached.post = post @@ -751,6 +790,8 @@ func (f *Font) initializeTables(offset int, isDfont bool) (buf1 []byte, isPostSc f.cmap = table{o, n} case 0x676c7966: f.glyf = table{o, n} + case 0x47504f53: + f.gpos = table{o, n} case 0x68656164: f.head = table{o, n} case 0x68686561: @@ -1488,10 +1529,32 @@ func (f *Font) GlyphAdvance(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, h font. // // It returns ErrNotFound if either glyph index is out of range. func (f *Font) Kern(b *Buffer, x0, x1 GlyphIndex, ppem fixed.Int26_6, h font.Hinting) (fixed.Int26_6, error) { - // TODO: how should this work with the GPOS table and CFF fonts? - // https://www.microsoft.com/typography/otspec/kern.htm says that - // "OpenType™ fonts containing CFF outlines are not supported by the 'kern' - // table and must use the 'GPOS' OpenType Layout table." + + // Use GPOS kern tables if available. + if f.cached.kernFuncs != nil { + for _, kf := range f.cached.kernFuncs { + adv, err := kf(x0, x1) + if err == ErrNotFound { + continue + } + if err != nil { + return 0, err + } + kern := fixed.Int26_6(adv) + kern = scale(kern*ppem, f.cached.unitsPerEm) + if h == font.HintingFull { + // Quantize the fixed.Int26_6 value to the nearest pixel. + kern = (kern + 32) &^ 63 + } + return kern, nil + } + return 0, ErrNotFound + } + + // Fallback to kern table. + + // TODO: Convert kern table handling into kernFunc and decide in Parse if + // GPOS or kern should be used. if n := f.NumGlyphs(); int(x0) >= n || int(x1) >= n { return 0, ErrNotFound