Files
ofdgo/ofdgo_font_match.go
xiaoqidun ef4028a2fc
build.yaml / build (push) Successful in 35s
feat(字体渲染): 改进字体匹配逻辑
2026-07-08 20:21:32 +08:00

385 lines
11 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.
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"path"
"strings"
)
type fontMatchRule struct {
Keys []string
Names []string
Files []string
System string
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", "fzxiaobiaosong*.ttf", "xiaobiaosong*.ttf"},
System: "FZXiaoBiaoSong-B05S",
NoSyntheticBold: true,
},
{
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{"黑体", "hei", "simhei", "heiti", "sthei", "方正黑体"},
Names: []string{"黑体", "SimHei", "HeiTi", "STHeiti", "方正黑体简体"},
Files: []string{"simhei.ttf", "STXIHEI.TTF", "FZHT_GB18030.TTF", "fzht*.ttf"},
System: "SimHei",
},
{
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{"仿宋", "fang", "fangsong", "simfang", "stfangsong", "方正仿宋"},
Names: []string{"仿宋", "FangSong", "SimFang", "STFangsong", "方正仿宋简体"},
Files: []string{"simfang.ttf", "fangsong.ttf", "STFANGSO.TTF", "FZFSJ.TTF", "fzfs*.ttf"},
System: "FangSong",
},
{
Keys: []string{"微软雅黑", "microsoftyahei", "yahei", "msyh"},
Names: []string{"微软雅黑", "Microsoft YaHei", "YaHei", "MSYH"},
Files: []string{"msyh.ttc", "msyh.ttf"},
System: "Microsoft YaHei",
},
{
Keys: []string{"arialblack"},
Names: []string{"Arial Black", "ArialBlack"},
Files: []string{"ariblk.ttf", "arial black*.ttf", "arialblack*.ttf"},
System: "Arial Black",
},
{
Keys: []string{"arial"},
Names: []string{"Arial"},
Files: []string{"arial.ttf", "arial*.ttf"},
System: "Arial",
},
{
Keys: []string{"helvetica"},
Names: []string{"Helvetica", "Arial"},
Files: []string{"arial.ttf", "arial*.ttf"},
System: "Arial",
},
{
Keys: []string{"couriernew", "courier"},
Names: []string{"Courier New", "CourierNew", "Courier"},
Files: []string{"cour.ttf", "courier new*.ttf", "courier*.ttf"},
System: "Courier New",
},
{
Keys: []string{"segoeui"},
Names: []string{"Segoe UI", "SegoeUI"},
Files: []string{"segoeui.ttf", "segoe*.ttf"},
System: "Segoe UI",
},
{
Keys: []string{"timesnewroman", "times"},
Names: []string{"Times New Roman", "TimesNewRoman"},
Files: []string{"times.ttf", "times new roman*.ttf", "times*.ttf"},
System: "Times New Roman",
},
}
var fontFallbackFiles = []string{
"simsun.ttc", "simsun.ttf", "nsimsun.ttf",
"msyh.ttc", "msyh.ttf", "simhei.ttf",
"NotoSansCJK*.ttc", "NotoSansSC*.otf", "SourceHanSansSC*.otf",
"arial.ttf",
}
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "", "", "", "")
// 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 字体名称
// 返回: string 规范化后的字体名称
func fontNormalizeName(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
name = strings.TrimSuffix(path.Base(name), path.Ext(name))
name = strings.ToLower(strings.TrimSpace(name))
return fontNameReplacer.Replace(name)
}
// fontCandidateNames 获取字体候选名称
// 入参: 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)
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)
}
}
}
}
}
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 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)
}
}
}
}
}
return result
}
// fontSystemNames 获取系统字体名称
// 入参: names 字体名称列表
// 返回: []string 系统字体名称
func fontSystemNames(names ...string) []string {
var result []string
seen := make(map[string]bool)
for _, name := range names {
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{
"SimSun", "Microsoft YaHei", "SimHei", "KaiTi", "FangSong",
"Noto Sans CJK SC", "Source Han Sans SC", "Arial", "Segoe UI", "Times New Roman",
}
}
// fontNoSyntheticBold 判断字体是否禁用合成粗体
// 入参: names 字体名称列表
// 返回: bool 是否禁用合成粗体
func fontNoSyntheticBold(names ...string) bool {
for _, name := range names {
for _, rule := range fontMatchRules {
level := fontRuleMatchLevel(rule, name)
if rule.NoSyntheticBold && level >= fontMatchExact && level <= fontMatchPartial {
return true
}
}
}
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
}
}
// fontRuleMatchLevel 获取字体规则匹配等级
// 入参: rule 字体匹配规则, name 字体名称
// 返回: int 匹配等级
func fontRuleMatchLevel(rule fontMatchRule, name string) int {
name = fontNormalizeName(name)
if name == "" {
return fontMatchNone
}
level := fontMatchNone
match := func(item string) bool {
item = fontNormalizeName(item)
if item != "" && name == item {
level = fontMatchExact
return true
}
if item != "" && (level == fontMatchNone || level > fontMatchPartial) && strings.HasPrefix(name, item) {
level = fontMatchPartial
}
if item != "" && level == fontMatchNone && strings.Contains(name, item) {
level = fontMatchFuzzy
}
return false
}
for _, item := range rule.Keys {
if match(item) {
return level
}
}
for _, item := range rule.Names {
if match(item) {
return level
}
}
if rule.System != "" {
match(rule.System)
}
return level
}
// appendFontName 追加字体名称
// 入参: names 字体名称列表, seen 去重映射, name 字体名称
// 返回: []string 字体名称列表
func appendFontName(names []string, seen map[string]bool, name string) []string {
name = strings.TrimSpace(name)
if name == "" {
return names
}
key := fontNormalizeName(name)
if key == "" || seen[key] {
return names
}
seen[key] = true
return append(names, name)
}
// appendFontPattern 追加字体文件匹配模式
// 入参: patterns 匹配模式列表, seen 去重映射, pattern 匹配模式
// 返回: []string 匹配模式列表
func appendFontPattern(patterns []string, seen map[string]bool, pattern string) []string {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
return patterns
}
key := strings.ToLower(pattern)
if seen[key] {
return patterns
}
seen[key] = true
return append(patterns, pattern)
}