feat(字体管理): 完善可变字体嵌入与裁剪
build.yaml / build (push) Successful in 47s

This commit is contained in:
xiaoqidun committed 2026-10-10 23:38:30 +08:00
1 parent 63e56c5ab8
commit 051fc3b65e
8 files changed
+505 -5

No files matched your search

+8 -1
View File
@@ -602,13 +602,20 @@ func parseCFFDict(data []byte) cffDict {
// 入参: data 字典数据
// 返回: cffDict 字典映射, error 编码或操作数错误
func readCFFDict(data []byte) (cffDict, error) {
return readCFFDictionary(data, false)
}
// readCFFDictionary 读取字体字典,CFF2顶层字典允许变化存储偏移操作符
// 入参: data 字典数据, cff2 是否为CFF2顶层字典
// 返回: cffDict 字典映射, error 编码或操作数错误
func readCFFDictionary(data []byte, cff2 bool) (cffDict, error) {
dict := make(cffDict)
var operands []float64
i := 0
for i < len(data) {
b := data[i]
i++
if b <= 21 {
if b <= 21 || cff2 && b == 24 {
op := int(b)
if b == 12 {
if i >= len(data) {
+311
View File
@@ -0,0 +1,311 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"encoding/binary"
"fmt"
"maps"
"math"
"github.com/go-text/typesetting/font/cff"
"github.com/go-text/typesetting/font/opentype"
"github.com/tdewolff/font"
)
// defaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓
// 入参: data 独立字体数据
// 返回: []byte 字体数据,非CFF2原样返回, error 解析或编码错误
func defaultCFF2Font(data []byte) (result []byte, err error) {
if len(data) < 12 || string(data[:4]) != "OTTO" {
return data, nil
}
found := false
for i, count := 0, int(binary.BigEndian.Uint16(data[4:])); i < count && i < (len(data)-12)/16; i++ {
if string(data[12+i*16:16+i*16]) == "CFF2" {
found = true
break
}
}
if !found {
return data, nil
}
table, err := fontFileTables(data, 0)
if err != nil || table["CFF2"] == nil {
return data, err
}
if table["CFF "] != nil || table["glyf"] != nil || len(table["head"]) < 54 || len(table["maxp"]) < 6 || len(table["hhea"]) < 36 {
return nil, fmt.Errorf("invalid CFF2 font tables")
}
defer func() {
if failure := recover(); failure != nil {
result, err = nil, fmt.Errorf("invalid CFF2 font: %v", failure)
}
}()
cffData := table["CFF2"]
if len(cffData) < 5 || cffData[0] != 2 || cffData[1] != 0 || cffData[2] < 5 || binary.BigEndian.Uint32(table["maxp"]) != 0x5000 {
return nil, fmt.Errorf("invalid CFF2 header")
}
start := int(cffData[2])
end := start + int(binary.BigEndian.Uint16(cffData[3:]))
if end > len(cffData) {
return nil, fmt.Errorf("invalid CFF2 dictionary length")
}
dict, err := readCFFDictionary(cffData[start:end], true)
if err != nil {
return nil, err
}
matrix, err := readCFFMatrix(dict, [6]float64{.001, 0, 0, .001, 0, 0})
units := binary.BigEndian.Uint16(table["head"][18:])
if err != nil || units < 16 || units > 16384 || matrix[0] != matrix[3] || matrix[1] != 0 || matrix[2] != 0 || matrix[4] != 0 || matrix[5] != 0 || math.Abs(matrix[0]*float64(units)-1) > 1e-6 {
return nil, fmt.Errorf("CFF2 FontMatrix does not match design units")
}
program, err := cff.ParseCFF2(table["CFF2"])
if err != nil {
return nil, err
}
if len(dict[24]) != 0 {
fvar := table["fvar"]
if len(fvar) < 16 || binary.BigEndian.Uint32(fvar) != 0x10000 || int(binary.BigEndian.Uint16(fvar[8:])) != program.VarStore.AxisCount() {
return nil, fmt.Errorf("CFF2 variation axis count mismatch")
}
}
count := int(binary.BigEndian.Uint16(table["maxp"][4:]))
metrics := int(binary.BigEndian.Uint16(table["hhea"][34:]))
if count == 0 || len(program.Charstrings) != count || metrics == 0 || metrics > count || len(table["hmtx"]) < metrics*4+(count-metrics)*2 {
return nil, fmt.Errorf("invalid CFF2 glyph metrics")
}
chars := make([][]byte, count)
for gid := range chars {
segments, _, err := program.LoadGlyph(uint16(gid), nil)
if err != nil {
return nil, fmt.Errorf("CFF2 glyph %d: %w", gid, err)
}
width := binary.BigEndian.Uint16(table["hmtx"][min(gid, metrics-1)*4:])
chars[gid], err = cffOutlineProgram(segments, width)
if err != nil {
return nil, fmt.Errorf("CFF2 glyph %d: %w", gid, err)
}
}
table["CFF "], err = staticCIDFont(table, chars)
if err != nil {
return nil, err
}
for _, tag := range []string{"CFF2", "fvar", "avar", "STAT", "HVAR", "VVAR", "MVAR", "DSIG"} {
delete(table, tag)
}
return serializeOTF(table)
}
// cffOutlineProgram 将默认设计轮廓编码为无子程序的Type2字形
// 入参: segments 字体设计坐标路径, width 水平字宽
// 返回: []byte 字形程序, error 坐标超出编码范围
func cffOutlineProgram(segments []opentype.Segment, width uint16) ([]byte, error) {
var output bytes.Buffer
if width < 32768 {
if err := encodeType2Number(&output, float64(width)); err != nil {
return nil, err
}
} else {
for range 2 {
if err := encodeType2Number(&output, float64(width)/2); err != nil {
return nil, err
}
}
output.Write([]byte{12, 10})
}
var x, y float64
for _, segment := range segments {
count, op := 1, byte(21)
switch segment.Op {
case opentype.SegmentOpMoveTo:
case opentype.SegmentOpLineTo:
op = 5
case opentype.SegmentOpCubeTo:
count, op = 3, 8
default:
return nil, fmt.Errorf("invalid CFF2 path operation")
}
for _, point := range segment.Args[:count] {
nx, ny := float64(point.X), float64(point.Y)
if err := encodeType2Number(&output, nx-x); err != nil {
return nil, err
}
if err := encodeType2Number(&output, ny-y); err != nil {
return nil, err
}
x, y = nx, ny
}
output.WriteByte(op)
}
output.WriteByte(14)
return output.Bytes(), nil
}
// staticCIDFont 封装独立Type2字形,CID与字形编号一致,不依赖字形名称
// 入参: table OpenType表, chars 字形程序
// 返回: []byte CFF字体, error 字体结构错误
func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
if len(chars) == 0 || len(chars) > 65535 || len(table["head"]) < 54 {
return nil, fmt.Errorf("invalid static CFF glyph count")
}
units := binary.BigEndian.Uint16(table["head"][18:])
if units < 16 || units > 16384 {
return nil, fmt.Errorf("invalid CFF design units")
}
name := fontFaceInfo(table["name"], 0).PostScriptName
if name == "" {
name = "OpenTypeInstance"
}
names := encodeCFFIndex([][]byte{[]byte(name)})
strings := encodeCFFIndex([][]byte{[]byte("Adobe"), []byte("Identity")})
charset := []byte{0}
for gid := 1; gid < len(chars); gid++ {
charset = binary.BigEndian.AppendUint16(charset, uint16(gid))
}
selects := []byte{3, 0, 1, 0, 0, 0}
selects = binary.BigEndian.AppendUint16(selects, uint16(len(chars)))
fonts := encodeCFFIndex([][]byte{encodeCFFDict(cffDict{18: {0, 0}})})
charIndex := encodeCFFIndex(chars)
scale := 1 / float64(units)
dict := cffDict{1230: {391, 392, 0}, 1234: {float64(len(chars))}, 1207: {scale, 0, 0, scale, 0, 0}, 15: {0}, 17: {0}, 1236: {0}, 1237: {0}}
for i := 0; i < 4; i++ {
dict[5] = append(dict[5], float64(int16(binary.BigEndian.Uint16(table["head"][36+2*i:]))))
}
var top []byte
for range 16 {
top = encodeCFFIndex([][]byte{encodeCFFDict(dict)})
offset := 4 + len(names) + len(top) + len(strings) + 2
if dict[15][0] == float64(offset) {
var output bytes.Buffer
for _, part := range [][]byte{{1, 0, 4, 4}, names, top, strings, {0, 0}, charset, selects, fonts, charIndex} {
output.Write(part)
}
return output.Bytes(), nil
}
dict[15][0] = float64(offset)
dict[1237][0] = float64(offset + len(charset))
dict[1236][0] = float64(offset + len(charset) + len(selects))
dict[17][0] = float64(offset + len(charset) + len(selects) + len(fonts))
}
return nil, fmt.Errorf("CFF offsets did not converge")
}
// staticCFFPrograms 读取无子程序和私有字典的CID字形,其他CFF保留原有保存策略
// 入参: sfnt 已解析字体
// 返回: [][]byte 独立字形程序,不符合条件时为空
func staticCFFPrograms(sfnt *font.SFNT) [][]byte {
data := sfnt.Tables["CFF "]
if len(data) < 4 {
return nil
}
_, end, err := readType2Index(data, int(data[2]))
if err != nil {
return nil
}
tops, end, err := readType2Index(data, end)
if err != nil || len(tops) != 1 {
return nil
}
_, end, err = readType2Index(data, end)
if err != nil {
return nil
}
globals, _, err := readType2Index(data, end)
if err != nil || len(globals) != 0 {
return nil
}
dict, err := readCFFDict(tops[0])
if err != nil || len(dict[1230]) != 3 {
return nil
}
matrix, err := readCFFMatrix(dict, [6]float64{.001, 0, 0, .001, 0, 0})
if err != nil || matrix[0] != matrix[3] || matrix[1] != 0 || matrix[2] != 0 || matrix[4] != 0 || matrix[5] != 0 || math.Abs(matrix[0]*float64(sfnt.UnitsPerEm())-1) > 1e-6 {
return nil
}
offset, err := cffDictOffset(data, dict, 1236)
if err != nil {
return nil
}
fds, _, err := readType2Index(data, offset)
if err != nil || len(fds) != 1 {
return nil
}
fd, err := readCFFDict(fds[0])
if err != nil || len(fd) != 1 || len(fd[18]) != 2 || fd[18][0] != 0 {
return nil
}
offset, err = cffDictOffset(data, dict, 17)
if err != nil {
return nil
}
chars, _, err := readType2Index(data, offset)
if err != nil || len(chars) != int(sfnt.NumGlyphs()) {
return nil
}
return chars
}
// subsetStaticCFFFont 裁剪静态CID字形并保留原编号、字符映射和度量
// 入参: sfnt 已解析字体, glyphs 使用的字形
// 返回: []byte 字体子集, error 裁剪错误
func subsetStaticCFFFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) {
chars := staticCFFPrograms(sfnt)
if chars == nil {
return nil, fmt.Errorf("CFF font cannot be subset safely")
}
keep := make(map[uint16]bool, len(glyphs)+1)
keep[0] = true
for _, gid := range glyphs {
if int(gid) >= len(chars) {
return nil, fmt.Errorf("CFF glyph index out of range")
}
keep[gid] = true
}
empty := make(map[uint16][]byte)
for gid := range chars {
if !keep[uint16(gid)] {
width := sfnt.GlyphAdvance(uint16(gid))
program := empty[width]
if program == nil {
var err error
program, err = cffOutlineProgram(nil, width)
if err != nil {
return nil, err
}
empty[width] = program
}
chars[gid] = program
}
}
table := maps.Clone(sfnt.Tables)
var err error
table["CFF "], err = staticCIDFont(table, chars)
if err != nil {
return nil, err
}
mapping := make(map[rune]uint16)
for gid := range keep {
for _, char := range sfnt.Cmap.ToUnicode(gid) {
mapping[char] = gid
}
}
table["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping)
for _, tag := range []string{"DSIG", "GSUB", "GPOS", "GDEF"} {
delete(table, tag)
}
return serializeOTF(table)
}
+4
View File
@@ -101,6 +101,10 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) {
if err != nil {
return "", err
}
data, err = defaultCFF2Font(data)
if err != nil {
return "", err
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil {
return "", err
+23 -2
View File
@@ -35,6 +35,7 @@ type sfntResourceMetrics struct{ *font.SFNT }
func (SFNTBackend) Name() string { return "sfnt" }
// OpenFontResource 提取独立字体并读取名称、样式和裁剪能力
// CFF2使用默认实例的静态轮廓,不改变原字体文件
// 入参: file 字体文件, index 集合索引
// 返回: *FontResource 字体资源, error 解析错误
func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, error) {
@@ -42,6 +43,10 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er
if err != nil {
return nil, err
}
data, err = defaultCFF2Font(data)
if err != nil {
return nil, err
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil {
return nil, err
@@ -91,7 +96,13 @@ func (f sfntResourceMetrics) Write() []byte { return fontSFNTData(f.SFNT) }
// 入参: sfnt 已解析字体
// 返回: bool 是否支持裁剪
func sfntSubsettable(sfnt *font.SFNT) bool {
return sfnt.IsTrueType && fontEmbeddingFlags(sfnt)&0x0100 == 0 && !slices.ContainsFunc([]string{"fvar", "COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil })
if fontEmbeddingFlags(sfnt)&0x0100 != 0 || slices.ContainsFunc([]string{"COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil }) {
return false
}
if sfnt.IsCFF {
return sfnt.Tables["fvar"] == nil && staticCFFPrograms(sfnt) != nil
}
return sfnt.IsTrueType && (sfnt.Tables["fvar"] == nil || variableSFNTSubsettable(sfnt))
}
// fontEmbeddingFlags 读取OpenType的嵌入标志,未提供OS/2表时返回零
@@ -133,10 +144,14 @@ func sfntFontName(sfnt *font.SFNT, names ...font.NameID) string {
return ""
}
// subsetSFNTFont 裁剪TrueType字形及复合依赖,保留字符映射、名称、度量和提示指令
// subsetSFNTFont 按实际用字裁剪字体,保留字符映射、名称及度量
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0, mapped 是否保留显式字形编号
// 返回: []byte 字体子集, error 错误信息
func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
data, err := defaultCFF2Font(data)
if err != nil {
return nil, err
}
sfnt, err := font.ParseSFNT(bytes.Clone(data), 0)
if err != nil {
return nil, err
@@ -144,6 +159,12 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
if fontEmbeddingFlags(sfnt)&0x0100 != 0 {
return bytes.Clone(data), nil
}
if sfnt.IsCFF {
return subsetStaticCFFFont(sfnt, glyphs)
}
if sfnt.Tables["fvar"] != nil {
return subsetVariableSFNTFont(sfnt, glyphs)
}
if mapped {
usage := newEditorFontUsage()
for _, id := range glyphs {
+6 -1
View File
@@ -23,6 +23,11 @@ import (
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
original := data
data, err := defaultCFF2Font(data)
if err != nil {
return false, original, nil, false, err
}
if len(data) < 12 {
return false, data, nil, false, nil
}
@@ -30,7 +35,7 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
isCFFSfnt := sfntTag == "OTTO"
numTables := binary.BigEndian.Uint16(data[4:6])
existingTables := make(map[string][]byte)
malformedDirectory := false
malformedDirectory := !bytes.Equal(original, data)
pos := 12
for i := 0; i < int(numTables); i++ {
if len(data) < pos+16 {
+1 -1
View File
@@ -292,7 +292,7 @@ func serializeOTF(tables map[string][]byte) ([]byte, error) {
searchRange := 1 << (entrySelector + 4)
rangeShift := int(numTables)*16 - searchRange
buf := new(bytes.Buffer)
if _, ok := tables["CFF "]; ok {
if tables["CFF "] != nil || tables["CFF2"] != nil {
buf.WriteString("OTTO")
} else {
binary.Write(buf, binary.BigEndian, uint32(0x00010000))
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"encoding/binary"
"fmt"
"github.com/tdewolff/font"
)
// variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构
// 入参: sfnt 已解析字体
// 返回: bool 是否具备可裁剪的变化数据
func variableSFNTSubsettable(sfnt *font.SFNT) bool {
fvar, gvar := sfnt.Tables["fvar"], sfnt.Tables["gvar"]
if len(fvar) < 16 || len(gvar) < 20 || binary.BigEndian.Uint32(fvar) != 0x10000 || binary.BigEndian.Uint32(gvar) != 0x10000 {
return false
}
axes := int(binary.BigEndian.Uint16(fvar[8:]))
offset, size := int(binary.BigEndian.Uint16(fvar[4:])), int(binary.BigEndian.Uint16(fvar[10:]))
return axes > 0 && size >= 20 && offset >= 16 && offset <= len(fvar) && axes <= (len(fvar)-offset)/size &&
int(binary.BigEndian.Uint16(gvar[4:])) == axes && binary.BigEndian.Uint16(gvar[12:]) == sfnt.NumGlyphs()
}
// subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据
// 入参: sfnt 已解析字体, glyphs 使用的字形
// 返回: []byte 字体子集, error 字形或变化表错误
func subsetVariableSFNTFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) {
if !variableSFNTSubsettable(sfnt) {
return nil, fmt.Errorf("invalid TrueType variation tables")
}
usage := newEditorFontUsage()
usage.glyphs[0] = true
for _, gid := range glyphs {
if gid >= sfnt.NumGlyphs() {
return nil, fmt.Errorf("variable font glyph index out of range")
}
usage.glyphs[gid] = true
for _, char := range sfnt.Cmap.ToUnicode(gid) {
usage.chars[char] = true
}
}
for gid := range usage.glyphs {
dependencies, err := sfnt.Glyf.Dependencies(gid)
if err != nil {
return nil, err
}
for _, dependency := range dependencies {
usage.glyphs[dependency] = true
}
}
mapping := make(map[rune]uint16, len(usage.chars))
for char := range usage.chars {
mapping[char] = sfnt.GlyphIndex(char)
}
table := make(map[string][]byte)
for _, tag := range []string{"head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp", "vhea", "vmtx"} {
if data := sfnt.Tables[tag]; data != nil {
table[tag] = data
}
}
table["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping)
static, err := serializeOTF(table)
if err != nil {
return nil, err
}
if subset := subsetSourceSFNTFont(static, usage); subset != nil {
table, err = fontFileTables(subset, 0)
if err != nil {
return nil, err
}
}
table["gvar"], err = subsetGlyphVariations(sfnt.Tables["gvar"], usage.glyphs)
if err != nil {
return nil, err
}
for _, tag := range []string{"fvar", "avar", "cvar", "HVAR", "VVAR", "MVAR", "STAT"} {
if data := sfnt.Tables[tag]; data != nil {
table[tag] = data
}
}
return serializeOTF(table)
}
// subsetGlyphVariations 按原字形编号重建gvar偏移表,保留共享元组及使用字形的完整变化数据
// 入参: data gvar表, glyphs 保留字形集合
// 返回: []byte 裁剪后的gvar表, error 表结构错误
func subsetGlyphVariations(data []byte, glyphs map[uint16]bool) ([]byte, error) {
if len(data) < 20 || binary.BigEndian.Uint32(data) != 0x10000 {
return nil, fmt.Errorf("invalid gvar header")
}
axes := uint64(binary.BigEndian.Uint16(data[4:]))
tuples := uint64(binary.BigEndian.Uint16(data[6:]))
shared := uint64(binary.BigEndian.Uint32(data[8:]))
count := int(binary.BigEndian.Uint16(data[12:]))
flags := binary.BigEndian.Uint16(data[14:])
start := uint64(binary.BigEndian.Uint32(data[16:]))
width := 2
if flags&1 != 0 {
width = 4
}
sharedSize := axes * tuples * 2
headerEnd := uint64(20 + (count+1)*width)
if flags > 1 || headerEnd > uint64(len(data)) || start < headerEnd || start > uint64(len(data)) || shared > uint64(len(data)) || sharedSize > uint64(len(data))-shared || sharedSize != 0 && shared < headerEnd {
return nil, fmt.Errorf("invalid gvar offsets")
}
readOffset := func(index int) uint64 {
if width == 2 {
return uint64(binary.BigEndian.Uint16(data[20+index*2:])) * 2
}
return uint64(binary.BigEndian.Uint32(data[20+index*4:]))
}
output := make([]byte, 20+(count+1)*4)
copy(output, data[:20])
binary.BigEndian.PutUint16(output[14:], 1)
binary.BigEndian.PutUint32(output[8:], uint32(len(output)))
output = append(output, data[shared:shared+sharedSize]...)
base := len(output)
binary.BigEndian.PutUint32(output[16:], uint32(base))
for gid := range count {
first, last := readOffset(gid), readOffset(gid+1)
if last < first || last > uint64(len(data))-start {
return nil, fmt.Errorf("invalid gvar glyph offsets")
}
if last > first && sharedSize != 0 && start+first < shared+sharedSize && shared < start+last {
return nil, fmt.Errorf("overlapping gvar glyph and shared tuple data")
}
binary.BigEndian.PutUint32(output[20+gid*4:], uint32(len(output)-base))
if glyphs[uint16(gid)] {
output = append(output, data[start+first:start+last]...)
}
}
binary.BigEndian.PutUint32(output[20+count*4:], uint32(len(output)-base))
return output, nil
}
+4
View File
@@ -56,6 +56,10 @@ func (b CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (Resolved
// 入参: data 字体数据
// 返回: FontMetrics 字体度量, error 解析错误
func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
data, err := defaultCFF2Font(data)
if err != nil {
return nil, err
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil {
return nil, err