package question import ( "bytes" "encoding/binary" "errors" ) // ttcFirstFont 从一个 TrueType Collection(.ttc)里提出第一个字面,返回等价的单字体数据。 // // 为什么需要:分享卡的字体候选里放了系统 CJK 字体路径,其中三个是 .ttc // (Debian / Ubuntu 的 fonts-noto-cjk 只提供 .ttc)。但 freetype.ParseFont 只能读 // 单字体文件,碰到 .ttc 一律失败 —— 现场表现是「路径明明存在,却报没有可用字体」 // (本地部署实测,分享卡 500)。这里按 sfnt 规范把选中的字面重排成独立文件: // 复制表目录与表数据,并把每张表的 offset 改写成新文件内的位置。 // // 只取第一个字面:分享卡只需要一套能画中英混排的字形,TTC 里后续字面是 // another weight / another language 的变体,不值得为它引入「按名字挑字面」的逻辑。 func ttcFirstFont(data []byte) ([]byte, error) { if len(data) < 16 || string(data[:4]) != "ttcf" { return nil, errors.New("not a TrueType Collection") } numFonts := binary.BigEndian.Uint32(data[8:12]) if numFonts == 0 { return nil, errors.New("TrueType Collection 里没有字体") } base := int(binary.BigEndian.Uint32(data[12:16])) if base < 0 || base+12 > len(data) { return nil, errors.New("TrueType Collection 的子字体偏移越界") } sfntVersion := binary.BigEndian.Uint32(data[base : base+4]) numTables := int(binary.BigEndian.Uint16(data[base+4 : base+6])) if numTables == 0 { return nil, errors.New("子字体没有表") } recStart := base + 12 if recStart+16*numTables > len(data) { return nil, errors.New("子字体的表目录越界") } type table struct { tag uint32 checksum uint32 srcOff int srcLen int dstOff uint32 } tables := make([]table, 0, numTables) for i := 0; i < numTables; i++ { p := recStart + 16*i srcOff := int(binary.BigEndian.Uint32(data[p+8 : p+12])) srcLen := int(binary.BigEndian.Uint32(data[p+12 : p+16])) if srcOff < 0 || srcLen < 0 || srcOff+srcLen > len(data) { return nil, errors.New("子字体的表数据越界") } tables = append(tables, table{ tag: binary.BigEndian.Uint32(data[p : p+4]), checksum: binary.BigEndian.Uint32(data[p+4 : p+8]), srcOff: srcOff, srcLen: srcLen, }) } // 新文件布局:sfnt header(12) + 表目录(16n) + 表数据(每张表 4 字节对齐) offset := uint32(12 + 16*numTables) for i := range tables { tables[i].dstOff = offset offset += uint32(tables[i].srcLen) offset += (4 - offset%4) % 4 } out := make([]byte, 0, offset) header := make([]byte, 12) binary.BigEndian.PutUint32(header[0:4], sfntVersion) binary.BigEndian.PutUint16(header[4:6], uint16(numTables)) // searchRange / entrySelector / rangeShift:规范要求填,部分解析器(含 freetype) // 不校验,这里仍按公式算,避免换解析器后踩坑。 maxPow2, entrySelector := 1, uint16(0) for maxPow2*2 <= numTables { maxPow2 *= 2 entrySelector++ } binary.BigEndian.PutUint16(header[6:8], uint16(maxPow2*16)) binary.BigEndian.PutUint16(header[8:10], entrySelector) binary.BigEndian.PutUint16(header[10:12], uint16(numTables*16-maxPow2*16)) out = append(out, header...) dir := make([]byte, 16*numTables) for i, t := range tables { p := 16 * i binary.BigEndian.PutUint32(dir[p:p+4], t.tag) binary.BigEndian.PutUint32(dir[p+4:p+8], t.checksum) binary.BigEndian.PutUint32(dir[p+8:p+12], t.dstOff) binary.BigEndian.PutUint32(dir[p+12:p+16], uint32(t.srcLen)) } out = append(out, dir...) var buf bytes.Buffer buf.Write(out) for _, t := range tables { if pad := int(t.dstOff) - buf.Len(); pad > 0 { buf.Write(make([]byte, pad)) } buf.Write(data[t.srcOff : t.srcOff+t.srcLen]) } return buf.Bytes(), nil }