Files
tamakyi e807dea218 feat(share-card): 字体自动发现 + 启动探测,缺字体不再等到第一次请求才 500
- 系统候选补上 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,中文与二维码渲染正常。
2026-10-27 12:37:48 +08:00

110 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}