feat(字体渲染): 统一字体处理位置

This commit is contained in:
2026-06-30 21:35:13 +08:00
parent 11b399829e
commit 615daf0dc3
5 changed files with 254 additions and 149 deletions
+7 -62
View File
@@ -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
+2 -7
View File
@@ -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]
}
+219
View File
@@ -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)
}
+1 -5
View File
@@ -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
}
+25 -75
View File
@@ -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 文件列表