- 系统候选补上 glyf 轮廓的 CJK 字体(文泉驿正黑/微米黑、DroidSansFallback、 AR PL UMing),并把 Noto Sans CJK 压到最后:它是 CFF/OTTO,freetype 必定解析失败, 排前面只会白白多读十几 MB - static/fonts 同时按当前工作目录与可执行文件目录查找,systemd 没配 WorkingDirectory 时也能找到(原先只按 CWD,文件在却报「没有可用字体」) - 启动时探一次字体:命中打 Info 带路径,缺失打 WARN 并给出补救命令 (apt install fonts-wqy-zenhei / 放 static/fonts/ / 配 font_path) - .ttc 先提取首个字面再用(仅对 glyf 轮廓的集合有效,如文泉驿); 失败结果缓存 1 分钟,避免公开端点被「每请求重新解析十几 MB」放大 实测:本地实例 apt install fonts-wqy-zenhei 后无需任何配置,启动即打出 font=/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc,/share-card 与单题分享图 从 500 变为 200 image/jpeg,中文与二维码渲染正常。
110 lines
3.7 KiB
Go
110 lines
3.7 KiB
Go
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
|
||
}
|