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
+45 -25
View File
@@ -112,8 +112,8 @@ func (fsys *FontFS) Glob(pattern string) ([]string, error) {
// 入参: names 字体名称列表
// 返回: string 匹配字体文件, bool 是否为名称匹配
func (fsys *FontFS) Match(names ...string) (string, bool) {
for _, name := range fontCandidateNames(names...) {
if matches := fsys.match(name + "*"); len(matches) > 0 {
for _, pattern := range fontFilePatterns(names...) {
if matches := fsys.match(pattern); len(matches) > 0 {
return matches[0], true
}
}
@@ -127,12 +127,16 @@ func (fsys *FontFS) Match(names ...string) (string, bool) {
// 入参: pattern 匹配模式
// 返回: []string 字体文件列表
func (fsys *FontFS) match(pattern string) []string {
var matches []string
buckets := make([][]string, fontMatchFuzzy+1)
for _, name := range fsys.names {
if matchFontPattern(pattern, name) {
matches = append(matches, name)
if rank := matchFontPatternRank(pattern, name); rank != fontMatchNone {
buckets[rank] = append(buckets[rank], name)
}
}
var matches []string
for rank := fontMatchExact; rank <= fontMatchFuzzy; rank++ {
matches = append(matches, buckets[rank]...)
}
return matches
}
@@ -140,8 +144,8 @@ func (fsys *FontFS) match(pattern string) []string {
// 返回: []string 字体文件列表
func (fsys *FontFS) fallbackFonts() []string {
for _, item := range fontFallbackFiles {
if _, ok := fsys.files[item]; ok {
return []string{item}
if matches := fsys.match(item); len(matches) > 0 {
return []string{matches[0]}
}
}
if len(fsys.names) > 0 {
@@ -179,33 +183,49 @@ func cleanFontName(name string) string {
return strings.ToLower(name)
}
// matchFontPattern 匹配字体文件模式
// matchFontPatternRank 获取字体文件匹配等级
// 入参: pattern 匹配模式, name 字体文件名
// 返回: bool 是否匹配
func matchFontPattern(pattern, name string) bool {
if ok, _ := path.Match(pattern, name); ok {
return true
}
if ok, _ := path.Match(strings.ToLower(pattern), strings.ToLower(name)); ok {
return true
}
stem := strings.TrimSuffix(pattern, "*")
stem = strings.TrimSuffix(stem, path.Ext(stem))
stem = fontNormalizeName(stem)
// 返回: int 匹配等级
func matchFontPatternRank(pattern, name string) int {
exactPath, _ := path.Match(pattern, name)
lowerPath, _ := path.Match(strings.ToLower(pattern), strings.ToLower(name))
stem := fontPatternStem(pattern)
if stem == "" {
return false
if exactPath || lowerPath {
return fontMatchFuzzy
}
return fontMatchNone
}
name = fontNormalizeName(name)
if strings.HasPrefix(name, stem) {
return true
if name == stem {
return fontMatchExact
}
for _, alias := range fontCandidateNames(stem) {
aliases := fontExactCandidateNames(stem)
for _, alias := range aliases {
alias = fontNormalizeName(alias)
if alias != "" && name == alias {
return fontMatchExact
}
}
if strings.HasPrefix(name, stem) {
return fontMatchPartial
}
for _, alias := range aliases {
alias = fontNormalizeName(alias)
if alias != "" && strings.HasPrefix(name, alias) {
return true
return fontMatchPartial
}
}
return false
if exactPath || lowerPath || strings.Contains(name, stem) {
return fontMatchFuzzy
}
for _, alias := range aliases {
alias = fontNormalizeName(alias)
if alias != "" && strings.Contains(name, alias) {
return fontMatchFuzzy
}
}
return fontMatchNone
}
// fontMemFile 内存字体文件
+50 -4
View File
@@ -157,6 +157,13 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
// 入参: names 字体名称列表
// 返回: string 匹配字体文件, bool 是否为名称匹配
func (r *Renderer) matchFont(names ...string) (string, bool) {
for _, dir := range r.fontDirs {
for _, pattern := range fontFilePatterns(names...) {
if matches := r.globFontFiles(dir, pattern); len(matches) > 0 {
return matches[0], true
}
}
}
for _, fsys := range r.fontFS {
if matcher, ok := fsys.(interface {
Match(...string) (string, bool)
@@ -170,11 +177,13 @@ func (r *Renderer) matchFont(names ...string) (string, bool) {
return matched, true
}
}
if !canLoadSystemFonts() {
for _, fsys := range r.fontFS {
if matched := fallbackFontFS(fsys); matched != "" {
return matched, false
}
}
}
return "", false
}
@@ -183,8 +192,7 @@ func (r *Renderer) matchFont(names ...string) (string, bool) {
// 返回: string 匹配字体文件
func matchFontFS(fsys fs.FS, names ...string) string {
for _, pattern := range fontFilePatterns(names...) {
matches, err := fs.Glob(fsys, pattern)
if err == nil && len(matches) > 0 {
if matches := fontFSMatches(fsys, pattern); len(matches) > 0 {
return matches[0]
}
}
@@ -195,13 +203,51 @@ func matchFontFS(fsys fs.FS, names ...string) string {
// 入参: fsys 字体文件系统
// 返回: string 回退字体文件
func fallbackFontFS(fsys fs.FS) string {
matches, err := fs.Glob(fsys, "*")
if err == nil && len(matches) > 0 {
for _, pattern := range fontFallbackFiles {
if matches := fontFSMatches(fsys, pattern); len(matches) > 0 {
return matches[0]
}
}
all, _ := fs.Glob(fsys, "*")
for _, name := range all {
if isFontFileName(name) {
return name
}
}
return ""
}
// fontFSMatches 匹配字体文件系统中的字体文件
// 入参: fsys 字体文件系统, pattern 匹配模式
// 返回: []string 字体文件列表
func fontFSMatches(fsys fs.FS, pattern string) []string {
seen := make(map[string]bool)
buckets := make([][]string, fontMatchFuzzy+1)
add := func(name string, rank int) {
if !isFontFileName(name) {
return
}
if rank == fontMatchNone || seen[name] {
return
}
seen[name] = true
buckets[rank] = append(buckets[rank], name)
}
matches, _ := fs.Glob(fsys, pattern)
for _, name := range matches {
add(name, matchFontPatternRank(pattern, path.Base(name)))
}
all, _ := fs.Glob(fsys, "*")
for _, name := range all {
add(name, matchFontPatternRank(pattern, path.Base(name)))
}
var result []string
for rank := fontMatchExact; rank <= fontMatchFuzzy; rank++ {
result = append(result, buckets[rank]...)
}
return result
}
// fontUsage 统计文档字体使用次数
// 入参: doc 文档结构
// 返回: map[string]int 字体使用次数
+173 -27
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,16 +159,35 @@ 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)
bases = append(bases, name)
}
for level := fontMatchExact; level <= maxLevel; level++ {
for _, name := range bases {
for _, rule := range fontMatchRules {
if fontRuleMatch(rule, name) {
if fontRuleMatchLevel(rule, name) == level {
result = appendFontName(result, seen, rule.System)
for _, item := range rule.Names {
result = appendFontName(result, seen, item)
@@ -136,27 +195,42 @@ 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...) {
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 fontRuleMatch(rule, name) {
if fontRuleMatchLevel(rule, name) == level {
for _, item := range rule.Files {
result = appendFontPattern(result, seen, item)
}
}
}
}
}
return result
}
@@ -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 追加字体名称
+41 -20
View File
@@ -470,6 +470,23 @@ func (r *Renderer) initCommon() {
r.defaultFontLoaded = r.loadDefaultFonts()
}
// childRenderer 创建继承当前配置的子渲染器
// 入参: reader 子阅读器
// 返回: *Renderer 子渲染器
func (r *Renderer) childRenderer(reader *Reader) *Renderer {
opts := []RendererOption{
WithDPI(r.DPI),
WithAnnotations(r.RenderAnnotations),
}
if len(r.fontDirs) > 0 {
opts = append(opts, WithFontDirs(r.fontDirs...))
}
if len(r.fontFS) > 0 {
opts = append(opts, WithFontFS(r.fontFS...))
}
return NewRenderer(reader, opts...)
}
// renderImage 渲染图片
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool) {
@@ -1194,8 +1211,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
}
for _, fsys := range r.fontFS {
for _, pattern := range fontFilePatterns(of.FontName, of.FamilyName) {
if matches, err := fs.Glob(fsys, pattern); err == nil {
for _, m := range matches {
for _, m := range fontFSMatches(fsys, pattern) {
resData, err := fs.ReadFile(fsys, m)
if err == nil {
if err := ff.LoadFont(resData, 0, fontStyle); err == nil {
@@ -1206,7 +1222,6 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
}
}
}
}
if !canLoadSystemFonts() {
for _, fsys := range r.fontFS {
if matches, err := fs.Glob(fsys, "*"); err == nil {
@@ -1262,13 +1277,29 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
// 入参: dir 目录, pattern 模式
// 返回: []string 文件列表
func (r *Renderer) globFontFiles(dir, pattern string) []string {
matches, _ := filepath.Glob(filepath.Join(dir, pattern))
var result []string
for _, m := range matches {
ext := strings.ToLower(filepath.Ext(m))
if ext == ".ttf" || ext == ".otf" || ext == ".ttc" {
result = append(result, m)
seen := make(map[string]bool)
buckets := make([][]string, fontMatchFuzzy+1)
add := func(name string, rank int) {
if !isFontFileName(name) {
return
}
if rank == fontMatchNone || seen[name] {
return
}
seen[name] = true
buckets[rank] = append(buckets[rank], name)
}
matches, _ := filepath.Glob(filepath.Join(dir, pattern))
for _, m := range matches {
add(m, matchFontPatternRank(pattern, filepath.Base(m)))
}
matches, _ = filepath.Glob(filepath.Join(dir, "*"))
for _, m := range matches {
add(m, matchFontPatternRank(pattern, filepath.Base(m)))
}
var result []string
for rank := fontMatchExact; rank <= fontMatchFuzzy; rank++ {
result = append(result, buckets[rank]...)
}
return result
}
@@ -1357,17 +1388,7 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
defer reader.Close()
doc, err := reader.Doc()
if err == nil {
opts := []RendererOption{
WithDPI(r.DPI),
WithAnnotations(r.RenderAnnotations),
}
if len(r.fontDirs) > 0 {
opts = append(opts, WithFontDirs(r.fontDirs...))
}
if len(r.fontFS) > 0 {
opts = append(opts, WithFontFS(r.fontFS...))
}
renderer := NewRenderer(reader, opts...)
renderer := r.childRenderer(reader)
for _, pageRef := range doc.Pages.Page {
content, err := reader.PageContent(pageRef)
if err != nil {