зеркало из https://github.com/golang/image.git
font/sfnt: support for kerning from GPOS tables
This CL adds support for reading kerning information from GPOS tables. Most modern fonts like Noto or Roboto do not provide a kern table. Instead they use the GPOS table for kerning. This CL is limited to horizontal kerning and latin scripts. The proprietary_test was extended to check all supported variations. gpos.go has full test coverage (excluding error conditions). Additionally, the new TestBulkKern (opt-in) can be used to check parsing of large font collection. Fixes: golang/go#29528 Change-Id: I9f1ae142c9e26170dd836ccaca1526bbefe9698a Reviewed-on: https://go-review.googlesource.com/c/159638 Reviewed-by: Nigel Tao <nigeltao@golang.org>
This commit is contained in:
Родитель
183bebdce1
Коммит
ef4a1470e0
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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},
|
||||
// <!-- GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA -->
|
||||
// <!-- GREEK UPSILON WITH HOOK SYMBOL -->
|
||||
// Last pair in second table
|
||||
{2048, font.HintingNone, [2]rune{'\u1f4d', '\u03d2'}, -10},
|
||||
},
|
||||
}
|
||||
|
||||
// proprietaryFDSelectTestCases hold a sample of each font's Font Dict Select
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче