diff --git a/font/sfnt/proprietary_test.go b/font/sfnt/proprietary_test.go index 25574ba..b534a5b 100644 --- a/font/sfnt/proprietary_test.go +++ b/font/sfnt/proprietary_test.go @@ -31,11 +31,13 @@ go test golang.org/x/image/font/sfnt -test.run=ProprietaryMicrosoft -args -propr // TODO: enable Apple/Microsoft tests by default on Darwin/Windows? import ( + "errors" "flag" "io/ioutil" "path/filepath" "testing" + "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) @@ -134,7 +136,7 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi if err != nil { t.Fatalf("Parse: %v", err) } - ppem := fixed.Int26_6(f.UnitsPerEm()) << 6 + ppem := fixed.Int26_6(f.UnitsPerEm()) qualifiedFilename := proprietor + "/" + filename var buf Buffer @@ -183,6 +185,33 @@ func testProprietary(t *testing.T, proprietor, filename string, minNumGlyphs, fi continue } } + +kernLoop: + for _, tc := range proprietaryKernTestCases[qualifiedFilename] { + var indexes [2]GlyphIndex + for i := range indexes { + x, err := f.GlyphIndex(&buf, tc.runes[i]) + if x == 0 && err == nil { + err = errors.New("no glyph index found") + } + if err != nil { + t.Errorf("GlyphIndex(%q): %v", tc.runes[0], err) + continue kernLoop + } + indexes[i] = x + } + kern, err := f.Kern(&buf, indexes[0], indexes[1], tc.ppem, tc.hinting) + if err != nil { + t.Errorf("Kern(%q, %q, ppem=%d, hinting=%v): %v", + tc.runes[0], tc.runes[1], tc.ppem, tc.hinting, err) + continue + } + if got := Units(kern); got != tc.want { + t.Errorf("Kern(%q, %q, ppem=%d, hinting=%v): got %d, want %d", + tc.runes[0], tc.runes[1], tc.ppem, tc.hinting, got, tc.want) + continue + } + } } // proprietaryVersions holds the expected version string of each proprietary @@ -282,3 +311,45 @@ var proprietaryGlyphIndexTestCases = map[string]map[rune]GlyphIndex{ '\uf042': 37, // PRIVATE USE AREA }, } + +type kernTestCase struct { + ppem fixed.Int26_6 + hinting font.Hinting + runes [2]rune + want Units +} + +// 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{ + "microsoft/Arial.ttf": { + {2048, font.HintingNone, [2]rune{'A', 'V'}, -152}, + // U+03B8 GREEK SMALL LETTER THETA + // U+03BB GREEK SMALL LETTER LAMDA + {2048, font.HintingNone, [2]rune{'\u03b8', '\u03bb'}, -39}, + {2048, font.HintingNone, [2]rune{'\u03bb', '\u03b8'}, -0}, + }, + "microsoft/Comic_Sans_MS.ttf": { + {2048, font.HintingNone, [2]rune{'A', 'V'}, 0}, + }, + "microsoft/Times_New_Roman.ttf": { + {768, font.HintingNone, [2]rune{'A', 'V'}, -99}, + {768, font.HintingFull, [2]rune{'A', 'V'}, -128}, + {2048, font.HintingNone, [2]rune{'A', 'A'}, 0}, + {2048, font.HintingNone, [2]rune{'A', 'T'}, -227}, + {2048, font.HintingNone, [2]rune{'A', 'V'}, -264}, + {2048, font.HintingNone, [2]rune{'T', 'A'}, -164}, + {2048, font.HintingNone, [2]rune{'T', 'T'}, 0}, + {2048, font.HintingNone, [2]rune{'T', 'V'}, 0}, + {2048, font.HintingNone, [2]rune{'V', 'A'}, -264}, + {2048, font.HintingNone, [2]rune{'V', 'T'}, 0}, + {2048, font.HintingNone, [2]rune{'V', 'V'}, 0}, + // U+0390 GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS + // U+0393 GREEK CAPITAL LETTER GAMMA + {2048, font.HintingNone, [2]rune{'\u0390', '\u0393'}, 0}, + {2048, font.HintingNone, [2]rune{'\u0393', '\u0390'}, 76}, + }, + "microsoft/Webdings.ttf": { + {2048, font.HintingNone, [2]rune{'\uf041', '\uf042'}, 0}, + }, +} diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go index b181688..79a9d14 100644 --- a/font/sfnt/sfnt.go +++ b/font/sfnt/sfnt.go @@ -23,6 +23,7 @@ import ( "errors" "io" + "golang.org/x/image/font" "golang.org/x/image/math/fixed" "golang.org/x/text/encoding/charmap" ) @@ -63,6 +64,7 @@ var ( errInvalidCmapTable = errors.New("sfnt: invalid cmap table") errInvalidGlyphData = errors.New("sfnt: invalid glyph data") errInvalidHeadTable = errors.New("sfnt: invalid head table") + errInvalidKernTable = errors.New("sfnt: invalid kern table") errInvalidLocaTable = errors.New("sfnt: invalid loca table") errInvalidLocationData = errors.New("sfnt: invalid location data") errInvalidMaxpTable = errors.New("sfnt: invalid maxp table") @@ -78,6 +80,7 @@ var ( errUnsupportedCmapEncodings = errors.New("sfnt: unsupported cmap encodings") errUnsupportedCompoundGlyph = errors.New("sfnt: unsupported compound glyph") 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") errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints") @@ -286,11 +289,17 @@ func ParseReaderAt(src io.ReaderAt) (*Font, error) { // The Font methods that don't take a *Buffer argument are always safe to call // concurrently. // -// Some methods provide lengths or co-ordinates, e.g. bounds, font metrics and +// Some methods provide lengths or coordinates, e.g. bounds, font metrics and // control points. All of these methods take a ppem parameter, which is the // number of pixels in 1 em, expressed as a 26.6 fixed point value. For // example, if 1 em is 10 pixels then ppem is fixed.I(10), which equals // fixed.Int26_6(10 << 6). +// +// To get those lengths or coordinates in terms of font units instead of +// pixels, use ppem = fixed.Int26_6(f.UnitsPerEm()) and if those methods take a +// font.Hinting parameter, use font.HintingNone. The return values will have +// type fixed.Int26_6, but those numbers can be converted back to Units with no +// further scaling necessary. type Font struct { src source @@ -327,12 +336,15 @@ type Font struct { // https://www.microsoft.com/typography/otspec/otff.htm#otttables // "Other OpenType Tables". // - // TODO: hdmx, kern, vmtx? Others? + // TODO: hdmx, vmtx? Others? + kern table cached struct { glyphIndex func(f *Font, b *Buffer, r rune) (GlyphIndex, error) indexToLocFormat bool // false means short, true means long. isPostScript bool + kernNumPairs int32 + kernOffset int32 postTableVersion uint32 unitsPerEm Units @@ -375,6 +387,10 @@ func (f *Font) initialize() error { if err != nil { return err } + buf, err = f.parseKern(buf) + if err != nil { + return err + } buf, err = f.parsePost(buf) if err != nil { return err @@ -444,6 +460,8 @@ func (f *Font) initializeTables(buf []byte) ([]byte, error) { f.hhea = table{o, n} case 0x686d7478: f.hmtx = table{o, n} + case 0x6b65726e: + f.kern = table{o, n} case 0x6c6f6361: f.loca = table{o, n} case 0x6d617870: @@ -542,6 +560,90 @@ func (f *Font) parseHead(buf []byte) ([]byte, error) { return buf, nil } +func (f *Font) parseKern(buf []byte) ([]byte, error) { + // https://www.microsoft.com/typography/otspec/kern.htm + + if f.kern.length == 0 { + return buf, nil + } + const headerSize = 4 + if f.kern.length < headerSize { + return nil, errInvalidKernTable + } + buf, err := f.src.view(buf, int(f.kern.offset), headerSize) + if err != nil { + return nil, err + } + offset := int(f.kern.offset) + headerSize + length := int(f.kern.length) - headerSize + + switch version := u16(buf); version { + case 0: + // TODO: support numTables != 1. Testing that requires finding such a font. + if numTables := int(u16(buf[2:])); numTables != 1 { + return nil, errUnsupportedKernTable + } + return f.parseKernVersion0(buf, offset, length) + case 1: + // TODO: find such a (proprietary?) font, and support it. Both of + // https://www.microsoft.com/typography/otspec/kern.htm + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html + // say that such fonts work on Mac OS but not on Windows. + } + return nil, errUnsupportedKernTable +} + +func (f *Font) parseKernVersion0(buf []byte, offset, length int) ([]byte, error) { + const headerSize = 6 + if length < headerSize { + return nil, errInvalidKernTable + } + buf, err := f.src.view(buf, offset, headerSize) + if err != nil { + return nil, err + } + if version := u16(buf); version != 0 { + return nil, errUnsupportedKernTable + } + subtableLength := int(u16(buf[2:])) + if subtableLength < headerSize || length < subtableLength { + return nil, errInvalidKernTable + } + if coverageBits := buf[5]; coverageBits != 0x01 { + // We only support horizontal kerning. + return nil, errUnsupportedKernTable + } + offset += headerSize + length -= headerSize + subtableLength -= headerSize + + switch format := buf[4]; format { + case 0: + return f.parseKernFormat0(buf, offset, subtableLength) + case 2: + // TODO: find such a (proprietary?) font, and support it. + } + return nil, errUnsupportedKernTable +} + +func (f *Font) parseKernFormat0(buf []byte, offset, length int) ([]byte, error) { + const headerSize, entrySize = 8, 6 + if length < headerSize { + return nil, errInvalidKernTable + } + buf, err := f.src.view(buf, offset, headerSize) + if err != nil { + return nil, err + } + numPairs := u16(buf) + if length != headerSize+entrySize*int(numPairs) { + return nil, errInvalidKernTable + } + f.cached.kernNumPairs = int32(numPairs) + f.cached.kernOffset = int32(offset) + headerSize + return buf, nil +} + func (f *Font) parseMaxp(buf []byte) ([]byte, error) { // https://www.microsoft.com/typography/otspec/maxp.htm @@ -763,6 +865,64 @@ func (f *Font) GlyphName(b *Buffer, x GlyphIndex) (string, error) { } } +// Kern returns the horizontal adjustment for the kerning pair (x0, x1). A +// positive kern means to move the glyphs further apart. ppem is the number of +// pixels in 1 em. +// +// 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." + + if n := f.NumGlyphs(); int(x0) >= n || int(x1) >= n { + return 0, ErrNotFound + } + // Not every font has a kern table. If it doesn't, there's no need to + // allocate a Buffer. + if f.kern.length == 0 { + return 0, nil + } + if b == nil { + b = &Buffer{} + } + + key := uint32(x0)<<16 | uint32(x1) + lo, hi := int32(0), f.cached.kernNumPairs + for lo < hi { + i := (lo + hi) / 2 + + // TODO: this view call inside the inner loop can lead to many small + // reads instead of fewer larger reads, which can be expensive. We + // should be able to do better, although we don't want to make (one) + // arbitrarily large read. Perhaps we should round up reads to 4K or 8K + // chunks. For reference, Arial.ttf's kern table is 5472 bytes. + // Times_New_Roman.ttf's kern table is 5220 bytes. + const entrySize = 6 + buf, err := b.view(&f.src, int(f.cached.kernOffset+i*entrySize), entrySize) + if err != nil { + return 0, err + } + + k := u32(buf) + if k < key { + lo = i + 1 + } else if k > key { + hi = i + } else { + kern := fixed.Int26_6(int16(u16(buf[4:]))) + 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, nil +} + // Name returns the name value keyed by the given NameID. // // It returns ErrNotFound if there is no value for that key. diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go index 6f6fff9..85d96a9 100644 --- a/font/sfnt/sfnt_test.go +++ b/font/sfnt/sfnt_test.go @@ -15,69 +15,31 @@ import ( "golang.org/x/image/math/fixed" ) -func moveTo(xa, ya int) Segment { +func moveTo(xa, ya fixed.Int26_6) Segment { return Segment{ - Op: SegmentOpMoveTo, - Args: [6]fixed.Int26_6{ - 0: fixed.I(xa), - 1: fixed.I(ya), - }, + Op: SegmentOpMoveTo, + Args: [6]fixed.Int26_6{xa, ya}, } } -func moveTo26_6(xa, ya fixed.Int26_6) Segment { +func lineTo(xa, ya fixed.Int26_6) Segment { return Segment{ - Op: SegmentOpMoveTo, - Args: [6]fixed.Int26_6{ - 0: xa, - 1: ya, - }, + Op: SegmentOpLineTo, + Args: [6]fixed.Int26_6{xa, ya}, } } -func lineTo(xa, ya int) Segment { +func quadTo(xa, ya, xb, yb fixed.Int26_6) Segment { return Segment{ - Op: SegmentOpLineTo, - Args: [6]fixed.Int26_6{ - 0: fixed.I(xa), - 1: fixed.I(ya), - }, + Op: SegmentOpQuadTo, + Args: [6]fixed.Int26_6{xa, ya, xb, yb}, } } -func lineTo26_6(xa, ya fixed.Int26_6) Segment { +func cubeTo(xa, ya, xb, yb, xc, yc fixed.Int26_6) Segment { return Segment{ - Op: SegmentOpLineTo, - Args: [6]fixed.Int26_6{ - 0: xa, - 1: ya, - }, - } -} - -func quadTo(xa, ya, xb, yb int) Segment { - return Segment{ - Op: SegmentOpQuadTo, - Args: [6]fixed.Int26_6{ - 0: fixed.I(xa), - 1: fixed.I(ya), - 2: fixed.I(xb), - 3: fixed.I(yb), - }, - } -} - -func cubeTo(xa, ya, xb, yb, xc, yc int) Segment { - return Segment{ - Op: SegmentOpCubeTo, - Args: [6]fixed.Int26_6{ - 0: fixed.I(xa), - 1: fixed.I(ya), - 2: fixed.I(xb), - 3: fixed.I(yb), - 4: fixed.I(xc), - 5: fixed.I(yc), - }, + Op: SegmentOpCubeTo, + Args: [6]fixed.Int26_6{xa, ya, xb, yb, xc, yc}, } } @@ -430,7 +392,7 @@ func testSegments(t *testing.T, filename string, wants [][]Segment) { if err != nil { t.Fatalf("Parse: %v", err) } - ppem := fixed.Int26_6(f.UnitsPerEm()) << 6 + ppem := fixed.Int26_6(f.UnitsPerEm()) if ng := f.NumGlyphs(); ng != len(wants) { t.Fatalf("NumGlyphs: got %d, want %d", ng, len(wants)) @@ -481,16 +443,16 @@ func TestPPEM(t *testing.T) { ppem fixed.Int26_6 want []Segment }{{ - ppem: fixed.I(12), + ppem: fixed.Int26_6(12 << 6), want: []Segment{ - moveTo26_6(77, 0), - lineTo26_6(77, 614), - lineTo26_6(230, 614), - lineTo26_6(230, 0), - lineTo26_6(77, 0), + moveTo(77, 0), + lineTo(77, 614), + lineTo(230, 614), + lineTo(230, 0), + lineTo(77, 0), }, }, { - ppem: fixed.I(2048), + ppem: fixed.Int26_6(2048), want: []Segment{ moveTo(205, 0), lineTo(205, 1638), diff --git a/font/sfnt/truetype.go b/font/sfnt/truetype.go index 4111feb..c0eefba 100644 --- a/font/sfnt/truetype.go +++ b/font/sfnt/truetype.go @@ -131,12 +131,12 @@ func appendGlyfSegments(dst []Segment, data []byte) ([]Segment, error) { // For simple (non-compound) glyphs, the remainder of the glyf data // consists of (flags, x, y) points: the Bézier curve segments. These are - // stored in columns (all the flags first, then all the x co-ordinates, - // then all the y co-ordinates), not rows, as it compresses better. + // stored in columns (all the flags first, then all the x coordinates, then + // all the y coordinates), not rows, as it compresses better. // // Decoding those points in row order involves two passes. The first pass // determines the indexes (relative to the data slice) of where the flags, - // the x co-ordinates and the y co-ordinates each start. + // the x coordinates and the y coordinates each start. flagIndex := int32(index) xIndex, yIndex, ok := findXYIndexes(data, index, numPoints) if !ok {