From ef4028a2fc85292b6192d1a1eca616099df2376f Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 8 Jul 2026 20:21:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E4=BD=93=E6=B8=B2=E6=9F=93):=20?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E5=AD=97=E4=BD=93=E5=8C=B9=E9=85=8D=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_font_fs.go | 194 +++++++++++++++++++++++++++++++++++++++++--- ofdgo_font_info.go | 57 ++++++------- ofdgo_font_match.go | 5 +- ofdgo_renderer.go | 102 +++++++++++------------ 4 files changed, 259 insertions(+), 99 deletions(-) diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index c4f85f2..9d5c9cb 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -112,10 +112,15 @@ func (fsys *FontFS) Glob(pattern string) ([]string, error) { // 入参: names 字体名称列表 // 返回: string 匹配字体文件, bool 是否为名称匹配 func (fsys *FontFS) Match(names ...string) (string, bool) { - for _, pattern := range fontFilePatterns(names...) { - if matches := fsys.match(pattern); len(matches) > 0 { - return matches[0], true - } + return fsys.MatchStyle(false, false, names...) +} + +// MatchStyle 匹配指定样式的字体名称 +// 入参: bold 是否粗体, italic 是否斜体, names 字体名称列表 +// 返回: string 匹配字体文件, bool 是否为名称匹配 +func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool) { + if matches := fsys.matchPatternsStyle(fontFilePatterns(names...), bold, italic); len(matches) > 0 { + return matches[0], true } if matches := fsys.fallbackFonts(); len(matches) > 0 { return matches[0], false @@ -127,17 +132,30 @@ func (fsys *FontFS) Match(names ...string) (string, bool) { // 入参: pattern 匹配模式 // 返回: []string 字体文件列表 func (fsys *FontFS) match(pattern string) []string { - buckets := make([][]string, fontMatchFuzzy+1) - for _, name := range fsys.names { - if rank := matchFontPatternRank(pattern, name); rank != fontMatchNone { - buckets[rank] = append(buckets[rank], name) + return fsys.matchStyle(pattern, false, false) +} + +// matchStyle 匹配指定样式的字体文件模式 +// 入参: pattern 匹配模式, bold 是否粗体, italic 是否斜体 +// 返回: []string 字体文件列表 +func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string { + return fsys.matchPatternsStyle([]string{pattern}, bold, italic) +} + +// matchPatternsStyle 匹配指定样式的字体文件模式 +// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体 +// 返回: []string 字体文件列表 +func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string { + var matches []fontFileMatch + seen := make(map[string]int) + for _, pattern := range patterns { + for _, name := range fsys.names { + rank := matchFontPatternRank(pattern, name) + appendFontFileMatch(&matches, seen, pattern, name, rank, bold, italic) } } - var matches []string - for rank := fontMatchExact; rank <= fontMatchFuzzy; rank++ { - matches = append(matches, buckets[rank]...) - } - return matches + sortFontFileMatches(matches, bold, italic) + return fontFileMatchNames(matches) } // fallbackFonts 获取回退字体文件 @@ -187,6 +205,7 @@ func cleanFontName(name string) string { // 入参: pattern 匹配模式, name 字体文件名 // 返回: int 匹配等级 func matchFontPatternRank(pattern, name string) int { + rawName := name exactPath, _ := path.Match(pattern, name) lowerPath, _ := path.Match(strings.ToLower(pattern), strings.ToLower(name)) stem := fontPatternStem(pattern) @@ -207,6 +226,9 @@ func matchFontPatternRank(pattern, name string) int { return fontMatchExact } } + if fontFileKnownStyleSuffix(fontFileStyleSuffix(pattern, rawName)) { + return fontMatchExact + } if strings.HasPrefix(name, stem) { return fontMatchPartial } @@ -228,6 +250,152 @@ func matchFontPatternRank(pattern, name string) int { return fontMatchNone } +type fontFileMatch struct { + name string + pattern string + rank int +} + +// appendFontFileMatch 追加字体文件匹配结果 +// 入参: matches 匹配结果, seen 已匹配文件, pattern 匹配模式, name 字体文件名, rank 匹配等级, bold 是否粗体, italic 是否斜体 +func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, pattern, name string, rank int, bold, italic bool) { + if rank == fontMatchNone || !isFontFileName(name) { + return + } + key := strings.ToLower(name) + next := fontFileMatch{name: name, pattern: pattern, rank: rank} + if index, ok := seen[key]; ok { + if fontFileMatchLess(next, (*matches)[index], bold, italic) { + (*matches)[index] = next + } + return + } + seen[key] = len(*matches) + *matches = append(*matches, next) +} + +// sortFontFileMatches 排序字体文件匹配结果 +// 入参: matches 匹配结果, bold 是否粗体, italic 是否斜体 +func sortFontFileMatches(matches []fontFileMatch, bold, italic bool) { + sort.SliceStable(matches, func(i, j int) bool { + return fontFileMatchLess(matches[i], matches[j], bold, italic) + }) +} + +// fontFileMatchLess 判断字体文件匹配结果优先级 +// 入参: left 左侧匹配结果, right 右侧匹配结果, bold 是否粗体, italic 是否斜体 +// 返回: bool 左侧是否优先 +func fontFileMatchLess(left, right fontFileMatch, bold, italic bool) bool { + if left.rank != right.rank { + return left.rank < right.rank + } + leftStyle := fontFileStyleRank(left.pattern, left.name, bold, italic) + rightStyle := fontFileStyleRank(right.pattern, right.name, bold, italic) + if leftStyle != rightStyle { + return leftStyle < rightStyle + } + return strings.ToLower(path.Base(left.name)) < strings.ToLower(path.Base(right.name)) +} + +// fontFileMatchNames 获取字体文件匹配名称 +// 入参: matches 匹配结果 +// 返回: []string 字体文件列表 +func fontFileMatchNames(matches []fontFileMatch) []string { + names := make([]string, 0, len(matches)) + for _, match := range matches { + names = append(names, match.name) + } + return names +} + +// fontFileStyleRank 获取字体文件样式匹配等级 +// 入参: pattern 匹配模式, name 字体文件名, bold 是否粗体, italic 是否斜体 +// 返回: int 样式匹配等级 +func fontFileStyleRank(pattern, name string, bold, italic bool) int { + fileBold, fileItalic := fontFileStyle(pattern, name) + rank := 0 + if fileBold != bold { + rank += 2 + } + if fileItalic != italic { + rank += 2 + } + suffix := fontFileStyleSuffix(pattern, name) + if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fontFileRegularSuffix(suffix) { + rank++ + } + return rank +} + +// fontFileStyle 获取字体文件样式 +// 入参: pattern 匹配模式, name 字体文件名 +// 返回: bool 是否粗体, bool 是否斜体 +func fontFileStyle(pattern, name string) (bool, bool) { + suffix := fontFileStyleSuffix(pattern, name) + bold := suffix == "b" || + suffix == "bd" || + suffix == "bold" || + suffix == "bi" || + suffix == "bolditalic" || + suffix == "boldoblique" + italic := suffix == "i" || + suffix == "it" || + suffix == "italic" || + suffix == "oblique" || + suffix == "bi" || + suffix == "bolditalic" || + suffix == "boldoblique" + return bold, italic +} + +// fontFileRegularSuffix 判断是否为常规样式后缀 +// 入参: suffix 样式后缀 +// 返回: bool 是否为常规样式后缀 +func fontFileRegularSuffix(suffix string) bool { + switch suffix { + case "r", "regular", "normal", "常规": + return true + default: + return false + } +} + +// fontFileKnownStyleSuffix 判断是否为已知样式后缀 +// 入参: suffix 样式后缀 +// 返回: bool 是否为已知样式后缀 +func fontFileKnownStyleSuffix(suffix string) bool { + switch suffix { + case "r", "regular", "normal", "常规", + "b", "bd", "bold", + "i", "it", "italic", "oblique", + "bi", "bolditalic", "boldoblique": + return true + default: + return false + } +} + +// fontFileStyleSuffix 获取字体文件样式后缀 +// 入参: pattern 匹配模式, name 字体文件名 +// 返回: string 样式后缀 +func fontFileStyleSuffix(pattern, name string) string { + key := fontNormalizeName(name) + stem := fontPatternStem(pattern) + if stem == "" || key == "" { + return "" + } + if strings.HasPrefix(key, stem) { + return strings.TrimPrefix(key, stem) + } + for _, alias := range fontExactCandidateNames(stem) { + alias = fontNormalizeName(alias) + if alias != "" && strings.HasPrefix(key, alias) { + return strings.TrimPrefix(key, alias) + } + } + return "" +} + // fontMemFile 内存字体文件 type fontMemFile struct { *bytes.Reader diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index f054eda..c95e2d5 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -157,11 +157,10 @@ func (r *Renderer) fontInfo(font Font) FontInfo { // 入参: names 字体名称列表 // 返回: string 匹配字体文件, bool 是否为名称匹配 func (r *Renderer) matchFont(names ...string) (string, bool) { + patterns := fontFilePatterns(names...) for _, dir := range r.fontDirs { - for _, pattern := range fontFilePatterns(names...) { - if matches := r.globFontFiles(dir, pattern); len(matches) > 0 { - return matches[0], true - } + if matches := r.matchFontFiles(dir, patterns, false, false); len(matches) > 0 { + return matches[0], true } } for _, fsys := range r.fontFS { @@ -191,10 +190,8 @@ func (r *Renderer) matchFont(names ...string) (string, bool) { // 入参: fsys 字体文件系统, names 字体名称列表 // 返回: string 匹配字体文件 func matchFontFS(fsys fs.FS, names ...string) string { - for _, pattern := range fontFilePatterns(names...) { - if matches := fontFSMatches(fsys, pattern); len(matches) > 0 { - return matches[0] - } + if matches := fontFSMatches(fsys, fontFilePatterns(names...)); len(matches) > 0 { + return matches[0] } return "" } @@ -204,7 +201,7 @@ func matchFontFS(fsys fs.FS, names ...string) string { // 返回: string 回退字体文件 func fallbackFontFS(fsys fs.FS) string { for _, pattern := range fontFallbackFiles { - if matches := fontFSMatches(fsys, pattern); len(matches) > 0 { + if matches := fontFSMatches(fsys, []string{pattern}); len(matches) > 0 { return matches[0] } } @@ -218,34 +215,32 @@ func fallbackFontFS(fsys fs.FS) string { } // fontFSMatches 匹配字体文件系统中的字体文件 -// 入参: fsys 字体文件系统, pattern 匹配模式 +// 入参: fsys 字体文件系统, patterns 匹配模式列表 // 返回: []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) { +func fontFSMatches(fsys fs.FS, patterns []string) []string { + return fontFSMatchesStyle(fsys, patterns, false, false) +} + +// fontFSMatchesStyle 匹配字体文件系统中的指定样式字体文件 +// 入参: fsys 字体文件系统, patterns 匹配模式列表, bold 是否粗体, italic 是否斜体 +// 返回: []string 字体文件列表 +func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []string { + var matches []fontFileMatch + seen := make(map[string]int) + add := func(pattern, name string, rank int) { if !isFontFileName(name) { return } - if rank == fontMatchNone || seen[name] { - return + appendFontFileMatch(&matches, seen, pattern, name, rank, bold, italic) + } + names, _ := fs.Glob(fsys, "*") + for _, pattern := range patterns { + for _, name := range names { + add(pattern, name, matchFontPatternRank(pattern, path.Base(name))) } - 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 + sortFontFileMatches(matches, bold, italic) + return fontFileMatchNames(matches) } // fontUsage 统计文档字体使用次数 diff --git a/ofdgo_font_match.go b/ofdgo_font_match.go index aa82718..acab535 100644 --- a/ofdgo_font_match.go +++ b/ofdgo_font_match.go @@ -151,6 +151,7 @@ func FontSystemNames(names ...string) []string { // 入参: 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) @@ -326,10 +327,10 @@ func fontRuleMatchLevel(rule fontMatchRule, name string) int { level = fontMatchExact return true } - if item != "" && (level == fontMatchNone || level > fontMatchPartial) && (strings.HasPrefix(name, item) || strings.HasPrefix(item, name)) { + if item != "" && (level == fontMatchNone || level > fontMatchPartial) && strings.HasPrefix(name, item) { level = fontMatchPartial } - if item != "" && level == fontMatchNone && (strings.Contains(name, item) || strings.Contains(item, name)) { + if item != "" && level == fontMatchNone && strings.Contains(name, item) { level = fontMatchFuzzy } return false diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 275c1a1..47f85cd 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -1269,26 +1269,24 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { if of.Italic { fontStyle |= canvas.FontItalic } + boldStyle := fontStyle&canvas.FontBold != 0 + italicStyle := fontStyle&canvas.FontItalic != 0 + patterns := fontFilePatterns(of.FontName, of.FamilyName) for _, dir := range r.fontDirs { - for _, pattern := range fontFilePatterns(of.FontName, of.FamilyName) { - matches := r.globFontFiles(dir, pattern) - for _, m := range matches { - if err := ff.LoadFontFile(m, fontStyle); err == nil { - r.FontMap[fontID] = ff - return ff - } + for _, m := range r.matchFontFiles(dir, patterns, boldStyle, italicStyle) { + if err := ff.LoadFontFile(m, fontStyle); err == nil { + r.FontMap[fontID] = ff + return ff } } } for _, fsys := range r.fontFS { - for _, pattern := range fontFilePatterns(of.FontName, of.FamilyName) { - for _, m := range fontFSMatches(fsys, pattern) { - resData, err := fs.ReadFile(fsys, m) - if err == nil { - if err := ff.LoadFont(resData, 0, fontStyle); err == nil { - r.FontMap[fontID] = ff - return ff - } + for _, m := range fontFSMatchesStyle(fsys, patterns, boldStyle, italicStyle) { + resData, err := fs.ReadFile(fsys, m) + if err == nil { + if err := ff.LoadFont(resData, 0, fontStyle); err == nil { + r.FontMap[fontID] = ff + return ff } } } @@ -1316,27 +1314,21 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { continue } for _, targetName := range fontSystemNames(name) { + for _, m := range r.matchFontFiles(systemFontDir(), fontFilePatterns(targetName), boldStyle, italicStyle) { + if err := ff.LoadFontFile(m, fontStyle); err == nil { + r.FontMap[fontID] = ff + return ff + } + } if err := ff.LoadSystemFont(targetName, fontStyle); err == nil { r.FontMap[fontID] = ff return ff } } - var sysFontDir string - switch runtime.GOOS { - case "linux": - sysFontDir = `/usr/share/fonts` - case "darwin": - sysFontDir = `/Library/Fonts` - default: - sysFontDir = `C:\Windows\Fonts` - } - for _, pattern := range fontFilePatterns(name) { - matches := r.globFontFiles(sysFontDir, pattern) - for _, m := range matches { - if err := ff.LoadFontFile(m, fontStyle); err == nil { - r.FontMap[fontID] = ff - return ff - } + for _, m := range r.matchFontFiles(systemFontDir(), fontFilePatterns(name), boldStyle, italicStyle) { + if err := ff.LoadFontFile(m, fontStyle); err == nil { + r.FontMap[fontID] = ff + return ff } } } @@ -1344,35 +1336,39 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { return defaultFont } -// globFontFiles 查找字体文件 -// 入参: dir 目录, pattern 模式 +// matchFontFiles 查找字体文件 +// 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体 // 返回: []string 文件列表 -func (r *Renderer) globFontFiles(dir, pattern string) []string { - seen := make(map[string]bool) - buckets := make([][]string, fontMatchFuzzy+1) - add := func(name string, rank int) { +func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bool) []string { + var matches []fontFileMatch + index := make(map[string]int) + add := func(pattern, name string, rank int) { if !isFontFileName(name) { return } - if rank == fontMatchNone || seen[name] { - return + appendFontFileMatch(&matches, index, pattern, name, rank, bold, italic) + } + files, _ := filepath.Glob(filepath.Join(dir, "*")) + for _, pattern := range patterns { + for _, name := range files { + add(pattern, name, matchFontPatternRank(pattern, filepath.Base(name))) } - 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))) + sortFontFileMatches(matches, bold, italic) + return fontFileMatchNames(matches) +} + +// systemFontDir 获取系统字体目录 +// 返回: string 字体目录 +func systemFontDir() string { + switch runtime.GOOS { + case "linux": + return `/usr/share/fonts` + case "darwin": + return `/Library/Fonts` + default: + return `C:\Windows\Fonts` } - 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 } // textObjectFontID 获取文本对象字体ID