blob: e2f1a30d43f18a0fb4120469a70047a3a7afa7f6 [file]
// Copyright 2026 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_test
import (
"encoding/binary"
"fmt"
"testing"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
)
func makeFontWithGPOSSubtable(subtableData []byte) ([]byte, error) {
gposHeader := []byte{
// GPOS Header
0x00, 0x01, 0x00, 0x00, // Version 1.0
0x00, 0x0a, // ScriptListOffset = 10
0x00, 0x1e, // FeatureListOffset = 30
0x00, 0x2c, // LookupListOffset = 44
// Offset 10: ScriptList
0x00, 0x01, // scriptCount = 1
0x44, 0x46, 0x4c, 0x54, // scriptTag = 'DFLT'
0x00, 0x08, // scriptOffset = 8 (points to Script table at 18)
// Offset 18: Script table
0x00, 0x04, // defaultLangSysOffset = 4 (points to LangSys table at 22)
0x00, 0x00, // langSysCount = 0
// Offset 22: LangSys table
0x00, 0x00, // lookupOrder = 0
0xff, 0xff, // requiredFeatureIndex = 0xffff
0x00, 0x01, // featureIndexCount = 1
0x00, 0x00, // featureIndices[0] = 0
// Offset 30: FeatureList
0x00, 0x01, // featureCount = 1
0x6b, 0x65, 0x72, 0x6e, // featureTag = 'kern'
0x00, 0x08, // featureOffset = 8 (points to Feature table at 38)
// Offset 38: Feature table
0x00, 0x00, // featureParamsOffset = 0
0x00, 0x01, // lookupCount = 1
0x00, 0x00, // lookupListIndices[0] = 0
// Offset 44: LookupList
0x00, 0x01, // lookupCount = 1
0x00, 0x04, // lookupOffsets[0] = 4 (points to Lookup table at 48)
// Offset 48: Lookup table
0x00, 0x02, // lookupType = 2 (PairPos)
0x00, 0x00, // lookupFlag = 0
0x00, 0x01, // subTableCount = 1
0x00, 0x08, // subTableOffsets[0] = 8 (points to Subtable at 56)
}
gposData := append(gposHeader, subtableData...)
base := goregular.TTF
if len(base) < 236 {
return nil, fmt.Errorf("base font too small")
}
numTables := binary.BigEndian.Uint16(base[4:6])
if numTables != 14 {
return nil, fmt.Errorf("expected 14 tables, got %d", numTables)
}
// Calculate new GPOS offset.
// It will go at the end of the file, after padding the base file to 4-byte alignment.
gposOffset := (len(base) + 16 + 3) &^ 3
padding := gposOffset - (len(base) + 16)
newFont := make([]byte, gposOffset+len(gposData))
copy(newFont[0:12], base[0:12])
binary.BigEndian.PutUint16(newFont[4:6], 15)
binary.BigEndian.PutUint16(newFont[6:8], 128)
binary.BigEndian.PutUint16(newFont[8:10], 3)
binary.BigEndian.PutUint16(newFont[10:12], 112)
// GPOS table record at index 0:
// tag: "GPOS" = 0x47504f53
// checksum: 0 (ignored by sfnt package)
// offset: GPOS offset
// length: length of GPOS table
binary.BigEndian.PutUint32(newFont[12:16], 0x47504f53)
binary.BigEndian.PutUint32(newFont[16:20], 0)
binary.BigEndian.PutUint32(newFont[20:24], uint32(gposOffset))
binary.BigEndian.PutUint32(newFont[24:28], uint32(len(gposData)))
// Shift original table records
for i := range 14 {
origRecordOffset := 12 + i*16
newRecordOffset := 28 + i*16
copy(newFont[newRecordOffset:newRecordOffset+16], base[origRecordOffset:origRecordOffset+16])
// Shift offset by 16
origOffset := binary.BigEndian.Uint32(base[origRecordOffset+8 : origRecordOffset+12])
binary.BigEndian.PutUint32(newFont[newRecordOffset+8:newRecordOffset+12], origOffset+16)
}
// Copy original table data
copy(newFont[252:], base[236:])
// Clear padding bytes
for i := range padding {
newFont[gposOffset-padding+i] = 0
}
// Copy GPOS table data
copy(newFont[gposOffset:], gposData)
return newFont, nil
}
func TestGPOSFormatErrors(t *testing.T) {
for _, test := range []struct {
desc string
subtable []byte
}{{
desc: "PairPos format 1",
subtable: []byte{
0x00, 0x01, // posFormat = 1
0x00, 0x0e, // coverageOffset = 14
0x00, 0x04, // valueFormat1 = 0x0004
0x00, 0x00, // valueFormat2 = 0
0x00, 0x02, // pairSetCount = 2
0x00, 0x16, // pairSetOffset[0] = 22
0x00, 0x18, // pairSetOffset[1] = 24
// Offset 14: Coverage (format 1, count 2, glyphs 0, 1)
0x00, 0x01, // format = 1
0x00, 0x02, // glyphCount = 2
0x00, 0x00, // glyphArray[0] = 0
0x00, 0x01, // glyphArray[1] = 1
// Offset 22: PairSet 0
0x03, 0xe8, // pairValueCount = 1000
// Offset 24: PairSet 1 (last PairSet)
0x00, 0x00, // pairValueCount = 0
},
}, {
desc: "PairPos format 2",
subtable: []byte{
0x00, 0x02, // posFormat = 2
0x00, 0x18, // coverageOffset = 24
0x00, 0x04, // valueFormat1 = 0x0004
0x00, 0x00, // valueFormat2 = 0
0x00, 0x22, // classDef1Offset = 34
0x00, 0x2a, // classDef2Offset = 42
0x00, 0x02, // class1Count = 2
0x00, 0x02, // class2Count = 2
// class1Records: 2 * 2 * 2 = 8 bytes of zeroes (offset 16-24)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Offset 24: Coverage (format 1, count 1, glyph 0)
0x00, 0x01, // format = 1
0x00, 0x01, // glyphCount = 1
0x00, 0x00, // glyphArray[0] = 0
// Offset 30: pad 4 bytes to reach offset 34
0x00, 0x00, 0x00, 0x00,
// Offset 34: ClassDef1 (format 1, startGlyph 0, count 1, class 999)
0x00, 0x01, // format = 1
0x00, 0x00, // startGlyph = 0
0x00, 0x01, // glyphCount = 1
0x03, 0xe7, // classValueArray[0] = 999
// Offset 42: ClassDef2 (format 1, startGlyph 0, count 1, class 999)
0x00, 0x01, // format = 1
0x00, 0x00, // startGlyph = 0
0x00, 0x01, // glyphCount = 1
0x03, 0xe7, // classValueArray[0] = 999
},
}} {
fontData, err := makeFontWithGPOSSubtable(test.subtable)
if err != nil {
t.Fatalf("failed to construct font: %v", err)
}
f, err := sfnt.Parse(fontData)
if err != nil {
t.Fatalf("failed to parse font: %v", err)
}
// Querying glyphs 0, 0 which maps pairValueCount = 1000 should return errInvalidGPOSTable
_, err = f.Kern(nil, 0, 0, fixed.I(20), font.HintingNone)
if err == nil {
t.Fatalf("f.Kern(nil, 0, 0, fixed.I(20), font.HintingNone): succeeded, want error")
}
}
}