mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
436 lines
14 KiB
Go
436 lines
14 KiB
Go
// 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 (
|
|
"bytes"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
|
|
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
|
)
|
|
|
|
// 字体匹配按精确、部分和模糊匹配排列,零值表示未匹配
|
|
const (
|
|
fontMatchNone = iota
|
|
fontMatchExact
|
|
fontMatchPartial
|
|
fontMatchFuzzy
|
|
)
|
|
|
|
// fontMatchRules 保存常用字体的名称、文件名及系统字体匹配规则
|
|
var fontMatchRules = normalizeFontMatchRules([]fontMatchRule{
|
|
{
|
|
Keys: []string{"小标宋", "方正小标宋", "xiaobiaosong", "fzxiaobiaosong", "fzxbs"},
|
|
Names: []string{"小标宋体", "方正小标宋简体", "FZXiaoBiaoSong-B05", "FZXiaoBiaoSong-B05S"},
|
|
Files: []string{"FZXBSJW.TTF", "fzxbs*.ttf", "fzxiaobiaosong*.ttf", "xiaobiaosong*.ttf"},
|
|
System: "FZXiaoBiaoSong-B05S",
|
|
NoSyntheticBold: true,
|
|
},
|
|
{
|
|
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{"黑体", "hei", "simhei", "heiti", "sthei", "方正黑体"},
|
|
Names: []string{"黑体", "SimHei", "HeiTi", "STHeiti", "方正黑体简体"},
|
|
Files: []string{"simhei.ttf", "STXIHEI.TTF", "FZHT_GB18030.TTF", "fzht*.ttf"},
|
|
System: "SimHei",
|
|
},
|
|
{
|
|
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{"仿宋", "fang", "fangsong", "simfang", "stfangsong", "方正仿宋"},
|
|
Names: []string{"仿宋", "FangSong", "SimFang", "STFangsong", "方正仿宋简体"},
|
|
Files: []string{"simfang.ttf", "fangsong.ttf", "STFANGSO.TTF", "FZFSJ.TTF", "fzfs*.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{"helvetica"},
|
|
Names: []string{"Helvetica", "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",
|
|
},
|
|
})
|
|
|
|
// fontMatchRule 字体匹配规则
|
|
type fontMatchRule struct {
|
|
Keys []string
|
|
Names []string
|
|
Files []string
|
|
System string
|
|
NoSyntheticBold bool
|
|
}
|
|
|
|
// FontNormalizeName 规范化字体名称
|
|
// 入参: name 字体名称
|
|
// 返回: string 规范化后的字体名称
|
|
func FontNormalizeName(name string) string {
|
|
return basefont.NormalizeName(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...)
|
|
}
|
|
|
|
// normalizeFontMatchRules 预处理固定字体规则的匹配名称,保留候选名称及顺序
|
|
// 入参: rules 字体匹配规则
|
|
// 返回: []fontMatchRule 预处理后的规则
|
|
func normalizeFontMatchRules(rules []fontMatchRule) []fontMatchRule {
|
|
for i := range rules {
|
|
rule := &rules[i]
|
|
keys := append(append(rule.Keys, rule.Names...), rule.System)
|
|
rule.Keys = nil
|
|
for _, key := range keys {
|
|
key = basefont.NormalizeName(key)
|
|
if key != "" && !slices.Contains(rule.Keys, key) {
|
|
rule.Keys = append(rule.Keys, key)
|
|
}
|
|
}
|
|
}
|
|
return rules
|
|
}
|
|
|
|
// fontCandidateNames 获取字体候选名称
|
|
// 入参: names 字体名称列表
|
|
// 返回: []string 字体候选名称
|
|
func fontCandidateNames(names ...string) []string {
|
|
return fontCandidateNamesByLevel(fontMatchFuzzy, names...)
|
|
}
|
|
|
|
// fontExactCandidateNames 获取精确字体候选名称
|
|
// 入参: names 字体名称列表
|
|
// 返回: []string 精确字体候选名称
|
|
func fontExactCandidateNames(names ...string) []string {
|
|
return fontCandidateNamesByLevel(fontMatchExact, names...)
|
|
}
|
|
|
|
// fontQualifiedNames 获取保留名称后缀的字体候选
|
|
// 入参: name 字体名称
|
|
// 返回: []string 保留后缀的候选名称
|
|
func fontQualifiedNames(name string) []string {
|
|
name = basefont.NormalizeName(name)
|
|
key := basefont.NormalizeName(name)
|
|
var names []string
|
|
seen := make(map[string]bool)
|
|
for _, rule := range fontMatchRules {
|
|
if fontRuleMatchLevel(rule, key) != fontMatchPartial {
|
|
continue
|
|
}
|
|
prefix := ""
|
|
for _, alias := range rule.Keys {
|
|
if len(alias) > len(prefix) && strings.HasPrefix(name, alias) {
|
|
prefix = alias
|
|
}
|
|
}
|
|
suffix := strings.TrimPrefix(name, prefix)
|
|
for _, alias := range append([]string{rule.System}, rule.Names...) {
|
|
names = basefont.AppendFontName(names, seen, basefont.NormalizeName(alias)+suffix)
|
|
}
|
|
}
|
|
return 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 = basefont.AppendFontName(result, seen, name)
|
|
bases = append(bases, name)
|
|
}
|
|
levels := fontMatchLevels(bases)
|
|
for level := fontMatchExact; level <= maxLevel; level++ {
|
|
for i := range bases {
|
|
for j, rule := range fontMatchRules {
|
|
if levels[i*len(fontMatchRules)+j] == level {
|
|
result = basefont.AppendFontName(result, seen, rule.System)
|
|
for _, item := range rule.Names {
|
|
result = basefont.AppendFontName(result, seen, item)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// fontPatternStem 获取字体匹配模式主干
|
|
// 入参: pattern 字体文件匹配模式
|
|
// 返回: string 字体匹配模式主干
|
|
func fontPatternStem(pattern string) string {
|
|
stem := strings.TrimSuffix(pattern, "*")
|
|
if basefont.IsFontFileName(stem) {
|
|
stem = strings.TrimSuffix(stem, path.Ext(stem))
|
|
}
|
|
if index := strings.IndexAny(stem, "*?["); index >= 0 {
|
|
stem = stem[:index]
|
|
}
|
|
return basefont.NormalizeName(stem)
|
|
}
|
|
|
|
// fontFilePatterns 获取字体文件匹配模式
|
|
// 入参: names 字体名称列表
|
|
// 返回: []string 字体文件匹配模式
|
|
func fontFilePatterns(names ...string) []string {
|
|
var result []string
|
|
seen := make(map[string]bool)
|
|
levels := fontMatchLevels(names)
|
|
for level := fontMatchExact; level <= fontMatchFuzzy; level++ {
|
|
for _, name := range fontCandidateNamesAtLevel(level, names, levels) {
|
|
result = appendFontPattern(result, seen, name+"*")
|
|
}
|
|
for i := range names {
|
|
for j, rule := range fontMatchRules {
|
|
if levels[i*len(fontMatchRules)+j] == level {
|
|
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 fontCandidateNamesByLevel(fontMatchPartial, name) {
|
|
result = basefont.AppendFontName(result, seen, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// fontCandidateNamesAtLevel 获取指定等级的字体候选名称
|
|
// 入参: level 匹配等级, names 字体名称列表, levels 名称与规则的匹配等级
|
|
// 返回: []string 字体候选名称
|
|
func fontCandidateNamesAtLevel(level int, names []string, levels []int) []string {
|
|
var result []string
|
|
seen := make(map[string]bool)
|
|
for i, name := range names {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
if level == fontMatchExact {
|
|
result = basefont.AppendFontName(result, seen, name)
|
|
}
|
|
for j, rule := range fontMatchRules {
|
|
if levels[i*len(fontMatchRules)+j] == level {
|
|
result = basefont.AppendFontName(result, seen, rule.System)
|
|
for _, item := range rule.Names {
|
|
result = basefont.AppendFontName(result, seen, item)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// fontDefaultSystemNames 获取默认系统字体名称
|
|
// 返回: []string 默认系统字体名称
|
|
func fontDefaultSystemNames() []string {
|
|
return []string{
|
|
"SimSun", "Microsoft YaHei", "SimHei", "KaiTi", "FangSong",
|
|
"Noto Sans CJK SC", "Source Han Sans SC", "Arial", "Segoe UI", "Times New Roman",
|
|
}
|
|
}
|
|
|
|
// fontNoSyntheticBold 判断字体是否禁用合成粗体
|
|
// 入参: names 字体名称列表
|
|
// 返回: bool 是否禁用合成粗体
|
|
func fontNoSyntheticBold(names ...string) bool {
|
|
for _, name := range names {
|
|
name = basefont.NormalizeName(name)
|
|
for _, rule := range fontMatchRules {
|
|
if !rule.NoSyntheticBold {
|
|
continue
|
|
}
|
|
level := fontRuleMatchLevel(rule, name)
|
|
if level >= fontMatchExact && level <= fontMatchPartial {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// fontMatchLevels 按名称和规则顺序计算匹配等级
|
|
// 入参: names 字体名称列表
|
|
// 返回: []int 名称与规则的匹配等级
|
|
func fontMatchLevels(names []string) []int {
|
|
levels := make([]int, len(names)*len(fontMatchRules))
|
|
for i, name := range names {
|
|
name = basefont.NormalizeName(name)
|
|
for j, rule := range fontMatchRules {
|
|
levels[i*len(fontMatchRules)+j] = fontRuleMatchLevel(rule, name)
|
|
}
|
|
}
|
|
return levels
|
|
}
|
|
|
|
// fontRuleMatchLevel 获取字体规则匹配等级
|
|
// 入参: rule 字体匹配规则, name 规范化后的字体名称
|
|
// 返回: int 匹配等级
|
|
func fontRuleMatchLevel(rule fontMatchRule, name string) int {
|
|
if name == "" {
|
|
return fontMatchNone
|
|
}
|
|
level := fontMatchNone
|
|
for _, item := range rule.Keys {
|
|
if name == item {
|
|
return fontMatchExact
|
|
}
|
|
if strings.HasPrefix(name, item) {
|
|
level = fontMatchPartial
|
|
} else if level == fontMatchNone && strings.Contains(name, item) {
|
|
level = fontMatchFuzzy
|
|
}
|
|
}
|
|
return level
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// appendFontFileNames 建立各字体的内部名称与文件别名匹配候选
|
|
// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称
|
|
// 返回: []fontFileCandidate 字体候选列表
|
|
func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]string) []fontFileCandidate {
|
|
file := candidates[index]
|
|
replaced := false
|
|
for face, names := range faces {
|
|
file.face = face
|
|
file.names = make([]string, len(names))
|
|
for i, name := range names {
|
|
file.names[i] = basefont.NormalizeName(name)
|
|
}
|
|
for _, name := range file.names {
|
|
file.normalized = name
|
|
if !replaced {
|
|
candidates[index] = file
|
|
replaced = true
|
|
} else {
|
|
candidates = append(candidates, file)
|
|
}
|
|
}
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
// fontCollectionIndex 按内部名称与样式选择集合项,未匹配时保留首项
|
|
// 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体
|
|
// 返回: int 零起始字体索引
|
|
func fontCollectionIndex(data []byte, names []string, bold, italic bool) int {
|
|
return fontNameIndex(basefont.FileNames(bytes.NewReader(data)), names, bold, italic)
|
|
}
|
|
|
|
// fontNameIndex 按名称与样式选择字体索引,未匹配时保留首项
|
|
// 入参: faces 各字体名称, names 目标名称, bold 是否粗体, italic 是否斜体
|
|
// 返回: int 零起始字体索引
|
|
func fontNameIndex(faces [][]string, names []string, bold, italic bool) int {
|
|
candidates := appendFontFileNames([]fontFileCandidate{{}}, 0, faces)
|
|
if matches := fontFileMatches(candidates, names, bold, italic); len(matches) > 0 {
|
|
return matches[0].face
|
|
}
|
|
return 0
|
|
}
|