mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-12 00:06:37 +08:00
232 lines
7.3 KiB
Go
232 lines
7.3 KiB
Go
package rule
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/binary"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 字体混淆还原(ttf.go / queryttf.go)的测试:用合成的 TTF 覆盖 cmap format 0/4、
|
||
// loca 短格式、简单/复合字形,验证「错误字体 → 正确字体」的按字形还原。
|
||
|
||
// buildTestTTF 构造一个最小可解析的 TTF:
|
||
// cmap(format 4)把给定码点映射到指定字形下标,glyf 里每个字形一个方框。
|
||
func buildTestTTF(t *testing.T, entries map[rune]uint16) []byte {
|
||
t.Helper()
|
||
const numGlyphs = 8
|
||
// 每个字形一个简单方框:轮廓数 1,1 个点。
|
||
glyph := func() []byte {
|
||
g := make([]byte, 0, 20)
|
||
g = append(g, 0x00, 0x01) // numberOfContours = 1
|
||
g = append(g, 0, 0, 0, 0, 0, 0, 0, 0) // bbox
|
||
g = append(g, 0x00, 0x00) // endPtsOfContours[0] = 0
|
||
g = append(g, 0x00, 0x00) // instructionLength = 0
|
||
g = append(g, 0x01, 0x01) // flag: on-curve | x-short | y-short
|
||
g = append(g, 0x00, 0x00) // x=0, y=0
|
||
return g
|
||
}
|
||
glyfData := make([]byte, 0, 128)
|
||
offsets := make([]int, 0, numGlyphs+1)
|
||
for i := 0; i < numGlyphs; i++ {
|
||
offsets = append(offsets, len(glyfData))
|
||
if i == 0 {
|
||
continue // .notdef 空字形
|
||
}
|
||
g := glyph()
|
||
// 让不同字形的字节长度不同,轮廓字符串才能区分。
|
||
for j := 0; j < i-1; j++ {
|
||
g = append(g, 0x01, 0x00, 0x00) // 额外点
|
||
}
|
||
glyfData = append(glyfData, g...)
|
||
}
|
||
offsets = append(offsets, len(glyfData))
|
||
|
||
// loca(短格式,偏移/2)
|
||
loca := make([]byte, (numGlyphs+1)*2)
|
||
for i, off := range offsets {
|
||
binary.BigEndian.PutUint16(loca[i*2:], uint16(off/2))
|
||
}
|
||
|
||
// cmap:format 4 单段 + 结束段
|
||
var cmap bytes.Buffer
|
||
codes := make([]rune, 0, len(entries))
|
||
for cp := range entries {
|
||
codes = append(codes, cp)
|
||
}
|
||
// 按码点排序,构造连续单点段。
|
||
for i := 0; i < len(codes); i++ {
|
||
for j := i + 1; j < len(codes); j++ {
|
||
if codes[j] < codes[i] {
|
||
codes[i], codes[j] = codes[j], codes[i]
|
||
}
|
||
}
|
||
}
|
||
segCount := len(codes) + 1
|
||
endCodes := make([]uint16, 0, segCount)
|
||
startCodes := make([]uint16, 0, segCount)
|
||
idDeltas := make([]uint16, 0, segCount)
|
||
for _, cp := range codes {
|
||
endCodes = append(endCodes, uint16(cp))
|
||
startCodes = append(startCodes, uint16(cp))
|
||
idDeltas = append(idDeltas, uint16(int(entries[cp])-int(cp)))
|
||
}
|
||
endCodes = append(endCodes, 0xFFFF)
|
||
startCodes = append(startCodes, 0xFFFF)
|
||
idDeltas = append(idDeltas, 1)
|
||
rangeOffsets := make([]uint16, segCount) // 全 0:用 idDelta
|
||
|
||
sub := new(bytes.Buffer)
|
||
writeU16 := func(v uint16) { _ = binary.Write(sub, binary.BigEndian, v) }
|
||
length := 16 + segCount*8
|
||
writeU16(4) // format
|
||
writeU16(uint16(length)) // length
|
||
writeU16(0) // language
|
||
writeU16(uint16(segCount * 2)) // segCountX2
|
||
writeU16(0) // searchRange(解析器不校验)
|
||
writeU16(0) // entrySelector
|
||
writeU16(0) // rangeShift
|
||
for _, v := range endCodes {
|
||
writeU16(v)
|
||
}
|
||
writeU16(0) // reservedPad
|
||
for _, v := range startCodes {
|
||
writeU16(v)
|
||
}
|
||
for _, v := range idDeltas {
|
||
writeU16(v)
|
||
}
|
||
for _, v := range rangeOffsets {
|
||
writeU16(v)
|
||
}
|
||
subBytes := sub.Bytes()
|
||
|
||
// cmap 头 + 一个子表记录
|
||
cmap.Write([]byte{0, 0})
|
||
_ = binary.Write(&cmap, binary.BigEndian, uint16(1))
|
||
_ = binary.Write(&cmap, binary.BigEndian, uint16(3)) // platformID = Windows
|
||
_ = binary.Write(&cmap, binary.BigEndian, uint16(1)) // encodingID = Unicode BMP
|
||
_ = binary.Write(&cmap, binary.BigEndian, uint32(12))
|
||
cmap.Write(subBytes)
|
||
|
||
head := make([]byte, 54)
|
||
binary.BigEndian.PutUint16(head[50:], 0) // indexToLocFormat = 0(短 loca)
|
||
maxp := make([]byte, 6)
|
||
binary.BigEndian.PutUint16(maxp[4:], numGlyphs)
|
||
|
||
tables := []struct {
|
||
tag string
|
||
data []byte
|
||
}{
|
||
{"cmap", cmap.Bytes()},
|
||
{"glyf", glyfData},
|
||
{"loca", loca},
|
||
{"head", head},
|
||
{"maxp", maxp},
|
||
}
|
||
|
||
var out bytes.Buffer
|
||
out.Write([]byte{0x00, 0x01, 0x00, 0x00}) // sfntVersion
|
||
_ = binary.Write(&out, binary.BigEndian, uint16(len(tables)))
|
||
_ = binary.Write(&out, binary.BigEndian, uint16(0))
|
||
_ = binary.Write(&out, binary.BigEndian, uint16(0))
|
||
_ = binary.Write(&out, binary.BigEndian, uint16(0))
|
||
offset := 12 + len(tables)*16
|
||
offsetsTable := make([]int, len(tables))
|
||
for i, tb := range tables {
|
||
padded := tb.data
|
||
if len(padded)%4 != 0 {
|
||
padded = append(padded, make([]byte, 4-len(padded)%4)...)
|
||
}
|
||
offsetsTable[i] = offset
|
||
offset += len(padded)
|
||
}
|
||
for i, tb := range tables {
|
||
out.WriteString(tb.tag)
|
||
_ = binary.Write(&out, binary.BigEndian, uint32(0))
|
||
_ = binary.Write(&out, binary.BigEndian, uint32(offsetsTable[i]))
|
||
_ = binary.Write(&out, binary.BigEndian, uint32(len(tb.data)))
|
||
}
|
||
for _, tb := range tables {
|
||
out.Write(tb.data)
|
||
for out.Len()%4 != 0 {
|
||
out.WriteByte(0)
|
||
}
|
||
}
|
||
return out.Bytes()
|
||
}
|
||
|
||
// 解析出的码点 → 字形 → 码点映射与构造时一致。
|
||
func TestQueryTTFParseRoundTrip(t *testing.T) {
|
||
data := buildTestTTF(t, map[rune]uint16{
|
||
'A': 1, 'B': 2, 'C': 3, 'D': 4,
|
||
})
|
||
font, err := parseQueryTTFFont(data)
|
||
if err != nil {
|
||
t.Fatalf("解析字体失败: %v", err)
|
||
}
|
||
for cp, gid := range map[rune]uint16{'A': 1, 'B': 2, 'C': 3, 'D': 4} {
|
||
if got := font.unicodeToGlyphID[cp]; got != gid {
|
||
t.Fatalf("码点 %q 的字形下标 = %d,期望 %d", cp, got, gid)
|
||
}
|
||
}
|
||
if font.glyphToUnicode[font.unicodeToGlyph['A']] != 'A' {
|
||
t.Fatal("字形 → 码点映射不正确")
|
||
}
|
||
if font.unicodeToGlyph['A'] == font.unicodeToGlyph['B'] {
|
||
t.Fatal("不同码点的轮廓不应相同")
|
||
}
|
||
}
|
||
|
||
// replaceFont:错误字体把 A 渲染成 B 的字形,正确字体应把 A 还原成 B。
|
||
func TestReplaceFontRestoresText(t *testing.T) {
|
||
// 错误字体:码点 A 指向字形 2(也就是 B 的形状)。
|
||
errorFontData := buildTestTTF(t, map[rune]uint16{'A': 2, 'B': 3, 'C': 4, 'D': 5})
|
||
// 正确字体:码点 B 指向字形 2。
|
||
correctFontData := buildTestTTF(t, map[rune]uint16{'A': 1, 'B': 2, 'C': 3, 'D': 4})
|
||
errorFont, err := parseQueryTTFFont(errorFontData)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
correctFont, err := parseQueryTTFFont(correctFontData)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 页面上写的是 'A',实际字形是 B → 应还原为 'B'。
|
||
got := replaceFontText("A", errorFont, correctFont, false)
|
||
if got != "B" {
|
||
t.Fatalf("replaceFont 还原结果 = %q,期望 %q", got, "B")
|
||
}
|
||
// 空白与未知码点保持原样。
|
||
mixed := replaceFontText("A 中", errorFont, correctFont, false)
|
||
if !strings.HasPrefix(mixed, "B ") || !strings.HasSuffix(mixed, "中") {
|
||
t.Fatalf("混合文本处理异常: %q", mixed)
|
||
}
|
||
// filter=true 时删掉没有对应字形的字符。
|
||
filtered := replaceFontText("A中", errorFont, correctFont, true)
|
||
if filtered != "B" {
|
||
t.Fatalf("filter 结果 = %q,期望 %q", filtered, "B")
|
||
}
|
||
}
|
||
|
||
// 坏字体只应报错,不能 panic。
|
||
func TestQueryTTFBadFontNoPanic(t *testing.T) {
|
||
cases := [][]byte{
|
||
nil,
|
||
[]byte("not a font"),
|
||
[]byte("ttcf"),
|
||
append([]byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x02}, make([]byte, 40)...),
|
||
}
|
||
for i, data := range cases {
|
||
if _, err := parseQueryTTFFont(data); err == nil {
|
||
t.Fatalf("坏字体 #%d 应返回错误", i)
|
||
}
|
||
}
|
||
// 截断的合法字体也不能 panic。
|
||
full := buildTestTTF(t, map[rune]uint16{'A': 1})
|
||
for cut := 1; cut < len(full); cut += 37 {
|
||
_, _ = parseQueryTTFFont(full[:cut])
|
||
}
|
||
}
|