feat(字体匹配): 改进字体匹配逻辑

This commit is contained in:
2026-07-02 23:12:49 +08:00
parent 69e5f3252c
commit 3de4d1c85a
4 changed files with 329 additions and 96 deletions
+183 -37
View File
@@ -27,36 +27,43 @@ type fontMatchRule struct {
NoSyntheticBold bool
}
const (
fontMatchNone = iota
fontMatchExact
fontMatchPartial
fontMatchFuzzy
)
var fontMatchRules = []fontMatchRule{
{
Keys: []string{"小标宋", "方正小标宋", "xiaobiaosong", "fzxiaobiaosong", "fzxbs"},
Names: []string{"小标宋体", "方正小标宋简体", "FZXiaoBiaoSong-B05", "FZXiaoBiaoSong-B05S"},
Files: []string{"FZXBSJW.TTF", "fzxbs*.ttf", "xiaobiaosong*.ttf"},
Files: []string{"FZXBSJW.TTF", "fzxbs*.ttf", "fzxiaobiaosong*.ttf", "xiaobiaosong*.ttf"},
System: "FZXiaoBiaoSong-B05S",
NoSyntheticBold: true,
},
{
Keys: []string{"宋体", "新宋体", "simsun", "nsimsun", "songti", "书宋", "方正书宋"},
Names: []string{"宋体", "新宋体", "SimSun", "NSimSun", "SongTi"},
Files: []string{"simsun.ttc", "simsun.ttf", "nsimsun.ttf"},
Keys: []string{"宋体", "新宋体", "simsun", "nsimsun", "simsunextb", "song", "songti", "stsong", "书宋", "方正书宋"},
Names: []string{"宋体", "新宋体", "SimSun", "NSimSun", "SongTi", "STSong"},
Files: []string{"simsun.ttc", "simsun.ttf", "nsimsun.ttf", "STSONG.TTF"},
System: "SimSun",
},
{
Keys: []string{"黑体", "simhei", "heiti"},
Names: []string{"黑体", "SimHei", "HeiTi"},
Files: []string{"simhei.ttf"},
Keys: []string{"黑体", "hei", "simhei", "heiti", "sthei", "方正黑体"},
Names: []string{"黑体", "SimHei", "HeiTi", "STHeiti", "方正黑体简体"},
Files: []string{"simhei.ttf", "STXIHEI.TTF", "FZHT_GB18030.TTF", "fzht*.ttf"},
System: "SimHei",
},
{
Keys: []string{"楷体", "kaiti", "simkai"},
Names: []string{"楷体", "KaiTi", "SimKai"},
Files: []string{"simkai.ttf", "kaiti.ttf"},
Keys: []string{"楷体", "kai", "kaiti", "simkai", "stkaiti", "方正楷体"},
Names: []string{"楷体", "KaiTi", "SimKai", "STKaiti", "方正楷体简体"},
Files: []string{"simkai.ttf", "kaiti.ttf", "STKAITI.TTF", "FZKTJ.TTF", "fzkai*.ttf", "fzkt*.ttf"},
System: "KaiTi",
},
{
Keys: []string{"仿宋", "fangsong", "simfang"},
Names: []string{"仿宋", "FangSong", "SimFang"},
Files: []string{"simfang.ttf", "fangsong.ttf"},
Keys: []string{"仿宋", "fang", "fangsong", "simfang", "stfangsong", "方正仿宋"},
Names: []string{"仿宋", "FangSong", "SimFang", "STFangsong", "方正仿宋简体"},
Files: []string{"simfang.ttf", "fangsong.ttf", "STFANGSO.TTF", "FZFSJ.TTF", "fzfs*.ttf"},
System: "FangSong",
},
{
@@ -103,7 +110,40 @@ var fontMatchRules = []fontMatchRule{
},
}
var fontFallbackFiles = []string{"simsun.ttc", "msyh.ttc", "simhei.ttf"}
var fontFallbackFiles = []string{
"simsun.ttc", "simsun.ttf", "nsimsun.ttf",
"msyh.ttc", "msyh.ttf", "simhei.ttf",
"NotoSansCJK*.ttc", "NotoSansSC*.otf", "SourceHanSansSC*.otf",
"arial.ttf",
}
// FontNormalizeName 规范化字体名称
// 入参: name 字体名称
// 返回: string 规范化后的字体名称
func FontNormalizeName(name string) string {
return fontNormalizeName(name)
}
// FontCandidateNames 获取字体候选名称
// 入参: names 字体名称列表
// 返回: []string 字体候选名称列表
func FontCandidateNames(names ...string) []string {
return fontCandidateNames(names...)
}
// FontFilePatterns 获取字体文件匹配模式
// 入参: names 字体名称列表
// 返回: []string 字体文件匹配模式
func FontFilePatterns(names ...string) []string {
return fontFilePatterns(names...)
}
// FontSystemNames 获取系统字体名称
// 入参: names 字体名称列表
// 返回: []string 系统字体名称
func FontSystemNames(names ...string) []string {
return fontSystemNames(names...)
}
// fontNormalizeName 规范化字体名称
// 入参: name 字体名称
@@ -119,19 +159,39 @@ func fontNormalizeName(name string) string {
// 入参: names 字体名称列表
// 返回: []string 字体候选名称
func fontCandidateNames(names ...string) []string {
return fontCandidateNamesByLevel(fontMatchFuzzy, names...)
}
// fontExactCandidateNames 获取精确字体候选名称
// 入参: names 字体名称列表
// 返回: []string 精确字体候选名称
func fontExactCandidateNames(names ...string) []string {
return fontCandidateNamesByLevel(fontMatchExact, names...)
}
// fontCandidateNamesByLevel 获取指定等级内的字体候选名称
// 入参: maxLevel 最大匹配等级, names 字体名称列表
// 返回: []string 字体候选名称
func fontCandidateNamesByLevel(maxLevel int, names ...string) []string {
var result []string
seen := make(map[string]bool)
var bases []string
for _, name := range names {
name = strings.TrimSpace(name)
if name == "" {
continue
}
result = appendFontName(result, seen, name)
for _, rule := range fontMatchRules {
if fontRuleMatch(rule, name) {
result = appendFontName(result, seen, rule.System)
for _, item := range rule.Names {
result = appendFontName(result, seen, item)
bases = append(bases, name)
}
for level := fontMatchExact; level <= maxLevel; level++ {
for _, name := range bases {
for _, rule := range fontMatchRules {
if fontRuleMatchLevel(rule, name) == level {
result = appendFontName(result, seen, rule.System)
for _, item := range rule.Names {
result = appendFontName(result, seen, item)
}
}
}
}
@@ -139,20 +199,34 @@ func fontCandidateNames(names ...string) []string {
return result
}
// fontPatternStem 获取字体匹配模式主干
// 入参: pattern 字体文件匹配模式
// 返回: string 字体匹配模式主干
func fontPatternStem(pattern string) string {
stem := strings.TrimSuffix(pattern, "*")
stem = strings.TrimSuffix(stem, path.Ext(stem))
if index := strings.IndexAny(stem, "*?["); index >= 0 {
stem = stem[:index]
}
return fontNormalizeName(stem)
}
// fontFilePatterns 获取字体文件匹配模式
// 入参: names 字体名称列表
// 返回: []string 字体文件匹配模式
func fontFilePatterns(names ...string) []string {
var result []string
seen := make(map[string]bool)
for _, name := range fontCandidateNames(names...) {
result = appendFontPattern(result, seen, name+"*")
}
for _, name := range names {
for _, rule := range fontMatchRules {
if fontRuleMatch(rule, name) {
for _, item := range rule.Files {
result = appendFontPattern(result, seen, item)
for level := fontMatchExact; level <= fontMatchFuzzy; level++ {
for _, name := range fontCandidateNamesAtLevel(level, names...) {
result = appendFontPattern(result, seen, name+"*")
}
for _, name := range names {
for _, rule := range fontMatchRules {
if fontRuleMatchLevel(rule, name) == level {
for _, item := range rule.Files {
result = appendFontPattern(result, seen, item)
}
}
}
}
@@ -167,19 +241,45 @@ func fontSystemNames(names ...string) []string {
var result []string
seen := make(map[string]bool)
for _, name := range names {
for _, item := range fontCandidateNames(name) {
for _, item := range fontCandidateNamesByLevel(fontMatchPartial, name) {
result = appendFontName(result, seen, item)
}
}
return result
}
// fontCandidateNamesAtLevel 获取指定等级的字体候选名称
// 入参: level 匹配等级, names 字体名称列表
// 返回: []string 字体候选名称
func fontCandidateNamesAtLevel(level int, names ...string) []string {
var result []string
seen := make(map[string]bool)
for _, name := range names {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if level == fontMatchExact {
result = appendFontName(result, seen, name)
}
for _, rule := range fontMatchRules {
if fontRuleMatchLevel(rule, name) == level {
result = appendFontName(result, seen, rule.System)
for _, item := range rule.Names {
result = appendFontName(result, seen, item)
}
}
}
}
return result
}
// fontDefaultSystemNames 获取默认系统字体名称
// 返回: []string 默认系统字体名称
func fontDefaultSystemNames() []string {
return []string{
"SimHei", "Microsoft YaHei", "SimSun", "KaiTi", "FangSong",
"Arial", "Segoe UI", "Times New Roman",
"SimSun", "Microsoft YaHei", "SimHei", "KaiTi", "FangSong",
"Noto Sans CJK SC", "Source Han Sans SC", "Arial", "Segoe UI", "Times New Roman",
}
}
@@ -189,7 +289,8 @@ func fontDefaultSystemNames() []string {
func fontNoSyntheticBold(names ...string) bool {
for _, name := range names {
for _, rule := range fontMatchRules {
if rule.NoSyntheticBold && fontRuleMatch(rule, name) {
level := fontRuleMatchLevel(rule, name)
if rule.NoSyntheticBold && level >= fontMatchExact && level <= fontMatchPartial {
return true
}
}
@@ -197,21 +298,66 @@ func fontNoSyntheticBold(names ...string) bool {
return false
}
// isFontFileName 判断是否为字体文件名
// 入参: name 文件名
// 返回: bool 是否为字体文件
func isFontFileName(name string) bool {
switch strings.ToLower(path.Ext(name)) {
case ".ttf", ".otf", ".ttc":
return true
default:
return false
}
}
// fontRuleMatch 判断字体规则是否匹配
// 入参: rule 字体匹配规则, name 字体名称
// 返回: bool 是否匹配
func fontRuleMatch(rule fontMatchRule, name string) bool {
return fontRuleMatchLevel(rule, name) != fontMatchNone
}
// fontRuleMatchLevel 获取字体规则匹配等级
// 入参: rule 字体匹配规则, name 字体名称
// 返回: int 匹配等级
func fontRuleMatchLevel(rule fontMatchRule, name string) int {
name = fontNormalizeName(name)
if name == "" {
return false
return fontMatchNone
}
for _, key := range rule.Keys {
key = fontNormalizeName(key)
if key != "" && (name == key || strings.Contains(name, key)) {
return true
items := fontRuleNames(rule)
for _, item := range items {
item = fontNormalizeName(item)
if item != "" && name == item {
return fontMatchExact
}
}
return false
for _, item := range items {
item = fontNormalizeName(item)
if item != "" && (strings.HasPrefix(name, item) || strings.HasPrefix(item, name)) {
return fontMatchPartial
}
}
for _, item := range items {
item = fontNormalizeName(item)
if item != "" && (strings.Contains(name, item) || strings.Contains(item, name)) {
return fontMatchFuzzy
}
}
return fontMatchNone
}
// fontRuleNames 获取字体规则名称列表
// 入参: rule 字体匹配规则
// 返回: []string 字体规则名称列表
func fontRuleNames(rule fontMatchRule) []string {
var result []string
result = append(result, rule.Keys...)
result = append(result, rule.Names...)
if rule.System != "" {
result = append(result, rule.System)
}
return result
}
// appendFontName 追加字体名称