diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index 865601d..c4f85f2 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -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 != "" && strings.HasPrefix(name, alias) { - return true + if alias != "" && name == alias { + return fontMatchExact } } - return false + if strings.HasPrefix(name, stem) { + return fontMatchPartial + } + for _, alias := range aliases { + alias = fontNormalizeName(alias) + if alias != "" && strings.HasPrefix(name, alias) { + return fontMatchPartial + } + } + 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 内存字体文件 diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index fb34f3c..2398917 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -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,9 +177,11 @@ func (r *Renderer) matchFont(names ...string) (string, bool) { return matched, true } } - for _, fsys := range r.fontFS { - if matched := fallbackFontFS(fsys); matched != "" { - return matched, false + 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 { - return 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 字体使用次数 diff --git a/ofdgo_font_match.go b/ofdgo_font_match.go index 3147d41..a37c6a7 100644 --- a/ofdgo_font_match.go +++ b/ofdgo_font_match.go @@ -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,19 +159,39 @@ 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) - for _, rule := range fontMatchRules { - if fontRuleMatch(rule, name) { - result = appendFontName(result, seen, rule.System) - for _, item := range rule.Names { - result = appendFontName(result, seen, item) + 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) + } } } } @@ -139,20 +199,34 @@ 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...) { - 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) + 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) + } } } } @@ -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 追加字体名称 diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 7ae9b48..6509179 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -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,14 +1211,12 @@ 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 { - 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 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 } } } @@ -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 {