summaryrefslogtreecommitdiff
path: root/mkvlib/parser/sfnt/gpos.go
diff options
context:
space:
mode:
Diffstat (limited to 'mkvlib/parser/sfnt/gpos.go')
-rw-r--r--mkvlib/parser/sfnt/gpos.go550
1 files changed, 550 insertions, 0 deletions
diff --git a/mkvlib/parser/sfnt/gpos.go b/mkvlib/parser/sfnt/gpos.go
new file mode 100644
index 0000000..e0e339c
--- /dev/null
+++ b/mkvlib/parser/sfnt/gpos.go
@@ -0,0 +1,550 @@
+// 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)
+ if err != nil {
+ return buf, nil, err
+ }
+
+ // 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)
+ if err != nil {
+ return buf, nil, err
+ }
+
+ 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 preceding 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
+ }
+}