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,中文与二维码渲染正常。
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
package question
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// buildTTC 拼一个最小的 TrueType Collection:两个字面共用一套表,
|
||||
// 表数据刻意按「与目录相反」的顺序摆放,用来证明提取逻辑是按目录里的 offset
|
||||
// 搬数据,而不是简单地顺序切片。
|
||||
func buildTTC(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
|
||||
// 表内容
|
||||
headData := []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
||||
cmapData := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
|
||||
|
||||
const ttcHeaderLen = 16
|
||||
const sfntHeaderLen = 12
|
||||
const recLen = 16
|
||||
const numTables = 2
|
||||
sfntStart := ttcHeaderLen
|
||||
dirStart := sfntStart + sfntHeaderLen
|
||||
dataStart := dirStart + recLen*numTables
|
||||
// 先放 cmap 再放 head(与目录顺序相反)
|
||||
cmapOff := dataStart
|
||||
headOff := cmapOff + len(cmapData)
|
||||
|
||||
buf := make([]byte, headOff+len(headData))
|
||||
|
||||
copy(buf[0:4], "ttcf")
|
||||
binary.BigEndian.PutUint32(buf[4:8], 0x00010000) // version
|
||||
binary.BigEndian.PutUint32(buf[8:12], 1) // numFonts
|
||||
binary.BigEndian.PutUint32(buf[12:16], uint32(sfntStart))
|
||||
|
||||
binary.BigEndian.PutUint32(buf[sfntStart:sfntStart+4], 0x00010000) // sfntVersion
|
||||
binary.BigEndian.PutUint16(buf[sfntStart+4:sfntStart+6], numTables)
|
||||
binary.BigEndian.PutUint16(buf[sfntStart+6:sfntStart+8], 16) // searchRange(随便填,提取时应重算)
|
||||
binary.BigEndian.PutUint16(buf[sfntStart+8:sfntStart+10], 0)
|
||||
binary.BigEndian.PutUint16(buf[sfntStart+10:sfntStart+12], 0)
|
||||
|
||||
// 第 1 条记录:head → 数据在末尾
|
||||
p := dirStart
|
||||
copy(buf[p:p+4], "head")
|
||||
binary.BigEndian.PutUint32(buf[p+4:p+8], 0x11111111) // checksum
|
||||
binary.BigEndian.PutUint32(buf[p+8:p+12], uint32(headOff))
|
||||
binary.BigEndian.PutUint32(buf[p+12:p+16], uint32(len(headData)))
|
||||
|
||||
// 第 2 条记录:cmap → 数据在前
|
||||
p = dirStart + recLen
|
||||
copy(buf[p:p+4], "cmap")
|
||||
binary.BigEndian.PutUint32(buf[p+4:p+8], 0x22222222)
|
||||
binary.BigEndian.PutUint32(buf[p+8:p+12], uint32(cmapOff))
|
||||
binary.BigEndian.PutUint32(buf[p+12:p+16], uint32(len(cmapData)))
|
||||
|
||||
copy(buf[cmapOff:], cmapData)
|
||||
copy(buf[headOff:], headData)
|
||||
return buf
|
||||
}
|
||||
|
||||
func TestTTCFirstFont(t *testing.T) {
|
||||
ttc := buildTTC(t)
|
||||
out, err := ttcFirstFont(ttc)
|
||||
require.NoError(t, err)
|
||||
|
||||
// sfnt header
|
||||
require.Equal(t, uint32(0x00010000), binary.BigEndian.Uint32(out[0:4]))
|
||||
require.Equal(t, uint16(2), binary.BigEndian.Uint16(out[4:6]))
|
||||
// searchRange = 最大 2 的幂 × 16 = 2 × 16 = 32;entrySelector = 1;rangeShift = 32-32 = 0
|
||||
require.Equal(t, uint16(32), binary.BigEndian.Uint16(out[6:8]))
|
||||
require.Equal(t, uint16(1), binary.BigEndian.Uint16(out[8:10]))
|
||||
require.Equal(t, uint16(0), binary.BigEndian.Uint16(out[10:12]))
|
||||
|
||||
// 目录:tag / checksum / length 原样保留,offset 指向新位置
|
||||
type rec struct {
|
||||
tag string
|
||||
checksum uint32
|
||||
off, ln uint32
|
||||
}
|
||||
var recs []rec
|
||||
for i := 0; i < 2; i++ {
|
||||
p := 12 + 16*i
|
||||
recs = append(recs, rec{
|
||||
tag: string(out[p : p+4]),
|
||||
checksum: binary.BigEndian.Uint32(out[p+4 : p+8]),
|
||||
off: binary.BigEndian.Uint32(out[p+8 : p+12]),
|
||||
ln: binary.BigEndian.Uint32(out[p+12 : p+16]),
|
||||
})
|
||||
}
|
||||
require.Equal(t, "head", recs[0].tag)
|
||||
require.Equal(t, "cmap", recs[1].tag)
|
||||
require.Equal(t, uint32(0x11111111), recs[0].checksum)
|
||||
require.Equal(t, uint32(0x22222222), recs[1].checksum)
|
||||
|
||||
// 表数据按新 offset 读出来必须与原始一致
|
||||
require.Equal(t, []byte{0xDE, 0xAD, 0xBE, 0xEF},
|
||||
out[recs[0].off:recs[0].off+recs[0].ln])
|
||||
require.Equal(t, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
|
||||
out[recs[1].off:recs[1].off+recs[1].ln])
|
||||
|
||||
// 每张表在新文件里都要 4 字节对齐(sfnt 规范要求)
|
||||
require.Zero(t, recs[0].off%4)
|
||||
require.Zero(t, recs[1].off%4)
|
||||
}
|
||||
|
||||
func TestTTCFirstFontRejects(t *testing.T) {
|
||||
cases := map[string][]byte{
|
||||
"空数据": nil,
|
||||
"不是 TTC": []byte("not a font file at all"),
|
||||
"头部被截断": []byte("ttcf\x00\x01\x00\x00"),
|
||||
"numFonts 为 0": func() []byte {
|
||||
b := make([]byte, 16)
|
||||
copy(b, "ttcf")
|
||||
binary.BigEndian.PutUint32(b[4:8], 0x00010000)
|
||||
return b
|
||||
}(),
|
||||
"子字体偏移越界": func() []byte {
|
||||
b := make([]byte, 16)
|
||||
copy(b, "ttcf")
|
||||
binary.BigEndian.PutUint32(b[4:8], 0x00010000)
|
||||
binary.BigEndian.PutUint32(b[8:12], 1)
|
||||
binary.BigEndian.PutUint32(b[12:16], 4096)
|
||||
return b
|
||||
}(),
|
||||
}
|
||||
for name, data := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := ttcFirstFont(data)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTTCFirstFontRejectsBadTableOffset 表目录里的 offset 指向文件外时必须报错,
|
||||
// 而不是把越界切片交给上层(否则就是一次 panic 而不是 500)。
|
||||
func TestTTCFirstFontRejectsBadTableOffset(t *testing.T) {
|
||||
ttc := buildTTC(t)
|
||||
// 第 1 条记录位于 ttcHeader(16) + sfntHeader(12),其 length 字段在记录内第 12 字节
|
||||
p := 16 + 12 + 12
|
||||
binary.BigEndian.PutUint32(ttc[p:p+4], 1<<20)
|
||||
|
||||
_, err := ttcFirstFont(ttc)
|
||||
require.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user