From 615daf0dc34ea377d2f8a0a8904f5d75d0e97a58 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Tue, 30 Jun 2026 21:35:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E4=BD=93=E6=B8=B2=E6=9F=93):=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=AD=97=E4=BD=93=E5=A4=84=E7=90=86=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_font_fs.go | 69 ++------------ ofdgo_font_info.go | 9 +- ofdgo_font_match.go | 219 +++++++++++++++++++++++++++++++++++++++++++ ofdgo_font_native.go | 6 +- ofdgo_renderer.go | 100 +++++--------------- 5 files changed, 254 insertions(+), 149 deletions(-) create mode 100644 ofdgo_font_match.go diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index f75d59b..865601d 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -112,10 +112,7 @@ func (fsys *FontFS) Glob(pattern string) ([]string, error) { // 入参: names 字体名称列表 // 返回: string 匹配字体文件, bool 是否为名称匹配 func (fsys *FontFS) Match(names ...string) (string, bool) { - for _, name := range names { - if strings.TrimSpace(name) == "" { - continue - } + for _, name := range fontCandidateNames(names...) { if matches := fsys.match(name + "*"); len(matches) > 0 { return matches[0], true } @@ -142,8 +139,7 @@ func (fsys *FontFS) match(pattern string) []string { // fallbackFonts 获取回退字体文件 // 返回: []string 字体文件列表 func (fsys *FontFS) fallbackFonts() []string { - preferred := []string{"simsun.ttc", "msyh.ttc", "simhei.ttf"} - for _, item := range preferred { + for _, item := range fontFallbackFiles { if _, ok := fsys.files[item]; ok { return []string{item} } @@ -195,74 +191,23 @@ func matchFontPattern(pattern, name string) bool { } stem := strings.TrimSuffix(pattern, "*") stem = strings.TrimSuffix(stem, path.Ext(stem)) - stem = normalizeFontFileName(stem) + stem = fontNormalizeName(stem) if stem == "" { return false } - name = normalizeFontFileName(name) + name = fontNormalizeName(name) if strings.HasPrefix(name, stem) { return true } - for _, alias := range fontNameAliases(stem) { - if strings.HasPrefix(name, alias) { + for _, alias := range fontCandidateNames(stem) { + alias = fontNormalizeName(alias) + if alias != "" && strings.HasPrefix(name, alias) { return true } } return false } -// normalizeFontFileName 规范化字体文件名 -// 入参: name 字体文件名 -// 返回: string 规范化后的字体文件名 -func normalizeFontFileName(name string) string { - name = strings.TrimSuffix(path.Base(name), path.Ext(name)) - name = strings.ToLower(name) - replacer := strings.NewReplacer(" ", "", "-", "", "_", "") - return replacer.Replace(name) -} - -// fontNameAliases 获取字体名称别名 -// 入参: name 字体名称 -// 返回: []string 字体名称别名 -func fontNameAliases(name string) []string { - aliases := []struct { - Keys []string - Values []string - }{ - {[]string{"宋体", "新宋体", "simsun"}, []string{"simsun"}}, - {[]string{"黑体", "simhei"}, []string{"simhei"}}, - {[]string{"楷体", "kaiti", "simkai"}, []string{"simkai"}}, - {[]string{"仿宋", "fangsong", "simfang"}, []string{"simfang"}}, - {[]string{"微软雅黑", "microsoftyahei", "yahei", "msyh"}, []string{"msyh"}}, - } - var result []string - for _, alias := range aliases { - for _, key := range alias.Keys { - key = normalizeFontFileName(key) - if key != "" && (name == key || strings.Contains(name, key)) { - result = append(result, alias.Values...) - break - } - } - } - if strings.Contains(name, "宋") { - result = append(result, "simsun") - } - if strings.Contains(name, "黑") { - result = append(result, "simhei") - } - if strings.Contains(name, "楷") { - result = append(result, "simkai") - } - if strings.Contains(name, "仿") { - result = append(result, "simfang") - } - if strings.Contains(name, "雅") { - result = append(result, "msyh") - } - return result -} - // fontMemFile 内存字体文件 type fontMemFile struct { *bytes.Reader diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index 01d6217..fb34f3c 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -19,7 +19,6 @@ import ( "io/fs" "path" "sort" - "strings" ) const ( @@ -183,12 +182,8 @@ func (r *Renderer) matchFont(names ...string) (string, bool) { // 入参: fsys 字体文件系统, names 字体名称列表 // 返回: string 匹配字体文件 func matchFontFS(fsys fs.FS, names ...string) string { - for _, name := range names { - name = strings.TrimSpace(name) - if name == "" { - continue - } - matches, err := fs.Glob(fsys, name+"*") + for _, pattern := range fontFilePatterns(names...) { + matches, err := fs.Glob(fsys, pattern) if err == nil && len(matches) > 0 { return matches[0] } diff --git a/ofdgo_font_match.go b/ofdgo_font_match.go new file mode 100644 index 0000000..9080558 --- /dev/null +++ b/ofdgo_font_match.go @@ -0,0 +1,219 @@ +// 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 +} + +var fontMatchRules = []fontMatchRule{ + { + Keys: []string{"宋体", "新宋体", "simsun", "nsimsun", "songti", "书宋", "方正书宋"}, + Names: []string{"宋体", "新宋体", "SimSun", "NSimSun", "SongTi"}, + Files: []string{"simsun.ttc", "simsun.ttf", "nsimsun.ttf"}, + System: "SimSun", + }, + { + Keys: []string{"黑体", "simhei", "heiti"}, + Names: []string{"黑体", "SimHei", "HeiTi"}, + Files: []string{"simhei.ttf"}, + System: "SimHei", + }, + { + Keys: []string{"楷体", "kaiti", "simkai"}, + Names: []string{"楷体", "KaiTi", "SimKai"}, + Files: []string{"simkai.ttf", "kaiti.ttf"}, + System: "KaiTi", + }, + { + Keys: []string{"仿宋", "fangsong", "simfang"}, + Names: []string{"仿宋", "FangSong", "SimFang"}, + Files: []string{"simfang.ttf", "fangsong.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{"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", "msyh.ttc", "simhei.ttf"} + +// fontNormalizeName 规范化字体名称 +// 入参: name 字体名称 +// 返回: string 规范化后的字体名称 +func fontNormalizeName(name string) string { + name = strings.TrimSuffix(path.Base(name), path.Ext(name)) + name = strings.ToLower(strings.TrimSpace(name)) + replacer := strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "") + return replacer.Replace(name) +} + +// fontCandidateNames 获取字体候选名称 +// 入参: names 字体名称列表 +// 返回: []string 字体候选名称 +func fontCandidateNames(names ...string) []string { + var result []string + seen := make(map[string]bool) + 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) + } + } + } + } + return result +} + +// 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) + } + } + } + } + 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 fontCandidateNames(name) { + 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", + } +} + +// fontRuleMatch 判断字体规则是否匹配 +// 入参: rule 字体匹配规则, name 字体名称 +// 返回: bool 是否匹配 +func fontRuleMatch(rule fontMatchRule, name string) bool { + name = fontNormalizeName(name) + if name == "" { + return false + } + for _, key := range rule.Keys { + key = fontNormalizeName(key) + if key != "" && (name == key || strings.Contains(name, key)) { + return true + } + } + return false +} + +// 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) +} diff --git a/ofdgo_font_native.go b/ofdgo_font_native.go index 3994178..2a8039b 100644 --- a/ofdgo_font_native.go +++ b/ofdgo_font_native.go @@ -21,11 +21,7 @@ import "github.com/tdewolff/canvas" // loadDefaultFonts 加载默认字体 // 返回: bool 是否加载成功 func (r *Renderer) loadDefaultFonts() bool { - sysFonts := []string{ - "SimHei", "Microsoft YaHei", "SimSun", "KaiTi", "FangSong", - "Arial", "Segoe UI", "Times New Roman", - } - for _, name := range sysFonts { + for _, name := range fontDefaultSystemNames() { if err := r.fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil { return true } diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index c3a9c97..42afb79 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -973,22 +973,26 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { fontStyle |= canvas.FontItalic } for _, dir := range r.fontDirs { - matches := r.globFontFiles(dir, of.FontName+"*") - for _, m := range matches { - if err := ff.LoadFontFile(m, fontStyle); err == nil { - r.FontMap[fontID] = ff - return ff + 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 _, fsys := range r.fontFS { - if matches, err := fs.Glob(fsys, of.FontName+"*"); err == nil { - for _, m := range matches { - 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 _, pattern := range fontFilePatterns(of.FontName, of.FamilyName) { + if matches, err := fs.Glob(fsys, pattern); err == nil { + for _, m := range matches { + resData, err := fs.ReadFile(fsys, m) + if err == nil { + if err := ff.LoadFont(resData, 0, fontStyle); err == nil { + r.FontMap[fontID] = ff + return ff + } } } } @@ -1016,13 +1020,8 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { if name == "" { continue } - targetName := fontAliasName(name) - if err := ff.LoadSystemFont(targetName, fontStyle); err == nil { - r.FontMap[fontID] = ff - return ff - } - if targetName != name { - if err := ff.LoadSystemFont(name, fontStyle); err == nil { + for _, targetName := range fontSystemNames(name) { + if err := ff.LoadSystemFont(targetName, fontStyle); err == nil { r.FontMap[fontID] = ff return ff } @@ -1036,23 +1035,13 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { default: sysFontDir = `C:\Windows\Fonts` } - matches := r.globFontFiles(sysFontDir, "*"+targetName+"*") - if len(matches) == 0 { - switch targetName { - case "SimSun": - matches = r.globFontFiles(sysFontDir, "simsun.ttc") - case "KaiTi": - matches = r.globFontFiles(sysFontDir, "simkai.ttf") - case "SimHei": - matches = r.globFontFiles(sysFontDir, "simhei.ttf") - case "FangSong": - matches = r.globFontFiles(sysFontDir, "simfang.ttf") - } - } - for _, m := range matches { - if err := ff.LoadFontFile(m, fontStyle); err == nil { - r.FontMap[fontID] = ff - return ff + 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 + } } } } @@ -1060,45 +1049,6 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { return defaultFont } -// fontAliasName 获取系统字体替代名称 -// 入参: name OFD字体名称 -// 返回: string 系统字体名称 -func fontAliasName(name string) string { - lower := strings.ToLower(strings.TrimSpace(name)) - aliases := map[string]string{ - "simhei": "SimHei", - "microsoft yahei": "Microsoft YaHei", - "simsun": "SimSun", - "kaiti": "KaiTi", - "fangsong": "FangSong", - "arial": "Arial", - "segoe ui": "Segoe UI", - "times new roman": "Times New Roman", - } - if mapped, ok := aliases[lower]; ok { - return mapped - } - contains := []struct { - Key string - Value string - }{ - {"黑体", "SimHei"}, - {"微软雅黑", "Microsoft YaHei"}, - {"仿宋", "FangSong"}, - {"楷体", "KaiTi"}, - {"方正书宋", "SimSun"}, - {"书宋", "SimSun"}, - {"宋体", "SimSun"}, - {"宋", "SimSun"}, - } - for _, item := range contains { - if strings.Contains(lower, item.Key) { - return item.Value - } - } - return name -} - // globFontFiles 查找字体文件 // 入参: dir 目录, pattern 模式 // 返回: []string 文件列表