mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:31:12 +08:00
1 parent
d07972346b
commit
70003486b8
37 files changed
+2386
-2509
No files matched your search
@@ -15,13 +15,147 @@
|
||||
package font
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 字体匹配按精确、部分和模糊匹配排列,零值表示未匹配
|
||||
const (
|
||||
MatchNone = iota
|
||||
MatchExact
|
||||
MatchPartial
|
||||
MatchFuzzy
|
||||
)
|
||||
|
||||
// matchRules 保存常用字体的名称、文件名及系统字体匹配规则
|
||||
var matchRules = normalizeMatchRules([]matchRule{
|
||||
{
|
||||
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",
|
||||
},
|
||||
})
|
||||
|
||||
// fontNameReplacer 移除字体名称匹配时忽略的分隔符
|
||||
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
|
||||
|
||||
// matchRule 字体匹配规则
|
||||
type matchRule struct {
|
||||
Keys []string
|
||||
Names []string
|
||||
Files []string
|
||||
System string
|
||||
NoSyntheticBold bool
|
||||
}
|
||||
|
||||
// FileCandidate 字体文件候选
|
||||
type FileCandidate struct {
|
||||
Name string
|
||||
Face int
|
||||
base string
|
||||
lowerBase string
|
||||
filename string
|
||||
normalized string
|
||||
names []string
|
||||
fold string
|
||||
}
|
||||
|
||||
// patternMatcher 字体文件匹配器
|
||||
type patternMatcher struct {
|
||||
pattern string
|
||||
lowerPattern string
|
||||
stem string
|
||||
aliases []string
|
||||
priority int
|
||||
}
|
||||
|
||||
// FileMatch 字体文件匹配结果
|
||||
type FileMatch struct {
|
||||
Name string
|
||||
Face int
|
||||
Priority int
|
||||
Rank int
|
||||
styleRank int
|
||||
sortName string
|
||||
}
|
||||
|
||||
// fileKey 字体文件与集合索引的去重键
|
||||
type fileKey struct {
|
||||
Name string
|
||||
Face int
|
||||
}
|
||||
|
||||
// NormalizeName 规范化字体名称
|
||||
// 入参: name 字体名称
|
||||
// 返回: string 规范化后的字体名称
|
||||
@@ -62,3 +196,611 @@ func AppendFontName(names []string, seen map[string]bool, name string) []string
|
||||
seen[key] = true
|
||||
return append(names, name)
|
||||
}
|
||||
|
||||
// normalizeMatchRules 预处理固定字体规则的匹配名称,保留候选名称及顺序
|
||||
// 入参: rules 字体匹配规则
|
||||
// 返回: []matchRule 预处理后的规则
|
||||
func normalizeMatchRules(rules []matchRule) []matchRule {
|
||||
for i := range rules {
|
||||
rule := &rules[i]
|
||||
keys := append(append(rule.Keys, rule.Names...), rule.System)
|
||||
rule.Keys = nil
|
||||
for _, key := range keys {
|
||||
key = NormalizeName(key)
|
||||
if key != "" && !slices.Contains(rule.Keys, key) {
|
||||
rule.Keys = append(rule.Keys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// CandidateNames 获取字体候选名称
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []string 字体候选名称
|
||||
func CandidateNames(names ...string) []string {
|
||||
return candidateNamesByLevel(MatchFuzzy, names...)
|
||||
}
|
||||
|
||||
// ExactCandidateNames 获取精确字体候选名称
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []string 精确字体候选名称
|
||||
func ExactCandidateNames(names ...string) []string {
|
||||
return candidateNamesByLevel(MatchExact, names...)
|
||||
}
|
||||
|
||||
// qualifiedNames 获取保留名称后缀的字体候选
|
||||
// 入参: name 字体名称
|
||||
// 返回: []string 保留后缀的候选名称
|
||||
func qualifiedNames(name string) []string {
|
||||
name = NormalizeName(name)
|
||||
key := NormalizeName(name)
|
||||
var names []string
|
||||
seen := make(map[string]bool)
|
||||
for _, rule := range matchRules {
|
||||
if ruleMatchLevel(rule, key) != MatchPartial {
|
||||
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 = AppendFontName(names, seen, NormalizeName(alias)+suffix)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// candidateNamesByLevel 获取指定等级内的字体候选名称
|
||||
// 入参: maxLevel 最大匹配等级, names 字体名称列表
|
||||
// 返回: []string 字体候选名称
|
||||
func candidateNamesByLevel(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)
|
||||
bases = append(bases, name)
|
||||
}
|
||||
levels := matchLevels(bases)
|
||||
for level := MatchExact; level <= maxLevel; level++ {
|
||||
for i := range bases {
|
||||
for j, rule := range matchRules {
|
||||
if levels[i*len(matchRules)+j] == level {
|
||||
result = AppendFontName(result, seen, rule.System)
|
||||
for _, item := range rule.Names {
|
||||
result = AppendFontName(result, seen, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// patternStem 获取字体匹配模式主干
|
||||
// 入参: pattern 字体文件匹配模式
|
||||
// 返回: string 字体匹配模式主干
|
||||
func patternStem(pattern string) string {
|
||||
stem := strings.TrimSuffix(pattern, "*")
|
||||
if IsFontFileName(stem) {
|
||||
stem = strings.TrimSuffix(stem, path.Ext(stem))
|
||||
}
|
||||
if index := strings.IndexAny(stem, "*?["); index >= 0 {
|
||||
stem = stem[:index]
|
||||
}
|
||||
return NormalizeName(stem)
|
||||
}
|
||||
|
||||
// FilePatterns 获取字体文件匹配模式
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []string 字体文件匹配模式
|
||||
func FilePatterns(names ...string) []string {
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
levels := matchLevels(names)
|
||||
for level := MatchExact; level <= MatchFuzzy; level++ {
|
||||
for _, name := range candidateNamesAtLevel(level, names, levels) {
|
||||
result = appendPattern(result, seen, name+"*")
|
||||
}
|
||||
for i := range names {
|
||||
for j, rule := range matchRules {
|
||||
if levels[i*len(matchRules)+j] == level {
|
||||
for _, item := range rule.Files {
|
||||
result = appendPattern(result, seen, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SystemNames 获取系统字体名称
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []string 系统字体名称
|
||||
func SystemNames(names ...string) []string {
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
for _, name := range names {
|
||||
for _, item := range candidateNamesByLevel(MatchPartial, name) {
|
||||
result = AppendFontName(result, seen, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// candidateNamesAtLevel 获取指定等级的字体候选名称
|
||||
// 入参: level 匹配等级, names 字体名称列表, levels 名称与规则的匹配等级
|
||||
// 返回: []string 字体候选名称
|
||||
func candidateNamesAtLevel(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 == MatchExact {
|
||||
result = AppendFontName(result, seen, name)
|
||||
}
|
||||
for j, rule := range matchRules {
|
||||
if levels[i*len(matchRules)+j] == level {
|
||||
result = AppendFontName(result, seen, rule.System)
|
||||
for _, item := range rule.Names {
|
||||
result = AppendFontName(result, seen, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// DefaultSystemNames 获取默认系统字体名称
|
||||
// 返回: []string 默认系统字体名称
|
||||
func DefaultSystemNames() []string {
|
||||
return []string{
|
||||
"SimSun", "Microsoft YaHei", "SimHei", "KaiTi", "FangSong",
|
||||
"Noto Sans CJK SC", "Source Han Sans SC", "Arial", "Segoe UI", "Times New Roman",
|
||||
}
|
||||
}
|
||||
|
||||
// NoSyntheticBold 判断字体是否禁用合成粗体
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: bool 是否禁用合成粗体
|
||||
func NoSyntheticBold(names ...string) bool {
|
||||
for _, name := range names {
|
||||
name = NormalizeName(name)
|
||||
for _, rule := range matchRules {
|
||||
if !rule.NoSyntheticBold {
|
||||
continue
|
||||
}
|
||||
level := ruleMatchLevel(rule, name)
|
||||
if level >= MatchExact && level <= MatchPartial {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchLevels 按名称和规则顺序计算匹配等级
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []int 名称与规则的匹配等级
|
||||
func matchLevels(names []string) []int {
|
||||
levels := make([]int, len(names)*len(matchRules))
|
||||
for i, name := range names {
|
||||
name = NormalizeName(name)
|
||||
for j, rule := range matchRules {
|
||||
levels[i*len(matchRules)+j] = ruleMatchLevel(rule, name)
|
||||
}
|
||||
}
|
||||
return levels
|
||||
}
|
||||
|
||||
// ruleMatchLevel 获取字体规则匹配等级
|
||||
// 入参: rule 字体匹配规则, name 规范化后的字体名称
|
||||
// 返回: int 匹配等级
|
||||
func ruleMatchLevel(rule matchRule, name string) int {
|
||||
if name == "" {
|
||||
return MatchNone
|
||||
}
|
||||
level := MatchNone
|
||||
for _, item := range rule.Keys {
|
||||
if name == item {
|
||||
return MatchExact
|
||||
}
|
||||
if strings.HasPrefix(name, item) {
|
||||
level = MatchPartial
|
||||
} else if level == MatchNone && strings.Contains(name, item) {
|
||||
level = MatchFuzzy
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
// appendPattern 追加字体文件匹配模式
|
||||
// 入参: patterns 匹配模式列表, seen 去重映射, pattern 匹配模式
|
||||
// 返回: []string 匹配模式列表
|
||||
func appendPattern(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)
|
||||
}
|
||||
|
||||
// AppendFileNames 建立各字体的内部名称与文件别名匹配候选
|
||||
// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称
|
||||
// 返回: []FileCandidate 字体候选列表
|
||||
func AppendFileNames(candidates []FileCandidate, index int, faces [][]string) []FileCandidate {
|
||||
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] = NormalizeName(name)
|
||||
}
|
||||
for _, name := range file.names {
|
||||
file.normalized = name
|
||||
if !replaced {
|
||||
candidates[index] = file
|
||||
replaced = true
|
||||
} else {
|
||||
candidates = append(candidates, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
// CollectionIndex 按内部名称与样式选择集合项,未匹配时保留首项
|
||||
// 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: int 零起始字体索引
|
||||
func CollectionIndex(data []byte, names []string, bold, italic bool) int {
|
||||
return NameIndex(FileNames(bytes.NewReader(data)), names, bold, italic)
|
||||
}
|
||||
|
||||
// NameIndex 按名称与样式选择字体索引,未匹配时保留首项
|
||||
// 入参: faces 各字体名称, names 目标名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: int 零起始字体索引
|
||||
func NameIndex(faces [][]string, names []string, bold, italic bool) int {
|
||||
candidates := AppendFileNames([]FileCandidate{{}}, 0, faces)
|
||||
if matches := FileMatches(candidates, names, bold, italic); len(matches) > 0 {
|
||||
return matches[0].Face
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// FileMatches 匹配字体文件候选
|
||||
// 入参: candidates 字体候选, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []FileMatch 按优先级排列的字体匹配
|
||||
func FileMatches(candidates []FileCandidate, names []string, bold, italic bool) []FileMatch {
|
||||
matches := make([]FileMatch, 0, len(candidates))
|
||||
seen := make(map[fileKey]int, len(candidates))
|
||||
if len(names) == 0 {
|
||||
for _, name := range DefaultSystemNames() {
|
||||
matches = append(matches, FileMatches(candidates, []string{name}, bold, italic)...)
|
||||
}
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
return matches[i].Rank < matches[j].Rank
|
||||
})
|
||||
unique := matches[:0]
|
||||
for _, match := range matches {
|
||||
key := fileKey{strings.ToLower(match.Name), match.Face}
|
||||
if _, ok := seen[key]; !ok {
|
||||
seen[key] = len(unique)
|
||||
unique = append(unique, match)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
for i, name := range names {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
for j, alias := range append([]string{name}, qualifiedNames(name)...) {
|
||||
matcher := patternMatcher{stem: NormalizeName(alias), priority: (i-len(names))*2 + min(j, 1)}
|
||||
if matcher.stem == "" {
|
||||
continue
|
||||
}
|
||||
for _, file := range candidates {
|
||||
if rank := matcher.rankCandidate(file); rank == MatchExact {
|
||||
appendFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, matcher := range newPatternMatchers(FilePatterns(names...)) {
|
||||
for _, file := range candidates {
|
||||
rank := matcher.rankCandidate(file)
|
||||
appendFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
||||
}
|
||||
}
|
||||
sortFileMatches(matches)
|
||||
return matches
|
||||
}
|
||||
|
||||
// FileCandidates 筛选字体文件候选
|
||||
// 入参: names 文件名列表, base 文件名提取函数
|
||||
// 返回: []FileCandidate 字体文件候选列表
|
||||
func FileCandidates(names []string, base func(string) string) []FileCandidate {
|
||||
files := make([]FileCandidate, 0, len(names))
|
||||
for _, name := range names {
|
||||
if IsFontFileName(name) {
|
||||
baseName := base(name)
|
||||
normalized := NormalizeName(strings.TrimSuffix(baseName, path.Ext(baseName)))
|
||||
files = append(files, FileCandidate{
|
||||
Name: name,
|
||||
base: baseName,
|
||||
lowerBase: strings.ToLower(baseName),
|
||||
filename: normalized,
|
||||
normalized: normalized,
|
||||
fold: strings.ToLower(name),
|
||||
})
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// newPatternMatcher 创建字体文件匹配器
|
||||
// 入参: pattern 匹配模式
|
||||
// 返回: patternMatcher 字体文件匹配器
|
||||
func newPatternMatcher(pattern string) patternMatcher {
|
||||
matcher := patternMatcher{
|
||||
pattern: pattern,
|
||||
lowerPattern: strings.ToLower(pattern),
|
||||
stem: patternStem(pattern),
|
||||
}
|
||||
if matcher.stem != "" {
|
||||
for _, alias := range ExactCandidateNames(matcher.stem) {
|
||||
alias = NormalizeName(alias)
|
||||
if alias != "" {
|
||||
matcher.aliases = append(matcher.aliases, alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matcher
|
||||
}
|
||||
|
||||
// newPatternMatchers 创建字体文件匹配器列表
|
||||
// 入参: patterns 匹配模式列表
|
||||
// 返回: []patternMatcher 字体文件匹配器列表
|
||||
func newPatternMatchers(patterns []string) []patternMatcher {
|
||||
matchers := make([]patternMatcher, 0, len(patterns))
|
||||
for priority, pattern := range patterns {
|
||||
matcher := newPatternMatcher(pattern)
|
||||
matcher.priority = priority
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
return matchers
|
||||
}
|
||||
|
||||
// rankCandidate 获取字体文件候选匹配等级
|
||||
// 入参: file 字体文件候选
|
||||
// 返回: int 匹配等级
|
||||
func (m patternMatcher) rankCandidate(file FileCandidate) int {
|
||||
exactPath, _ := path.Match(m.pattern, file.base)
|
||||
lowerPath, _ := path.Match(m.lowerPattern, file.lowerBase)
|
||||
if m.stem == "" {
|
||||
if exactPath || lowerPath {
|
||||
return MatchFuzzy
|
||||
}
|
||||
return MatchNone
|
||||
}
|
||||
name := m.candidateName(file)
|
||||
if name == m.stem {
|
||||
return MatchExact
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if name == alias {
|
||||
return MatchExact
|
||||
}
|
||||
}
|
||||
if fileKnownStyleSuffix(m.styleSuffixNormalized(name)) {
|
||||
return MatchExact
|
||||
}
|
||||
if strings.HasPrefix(name, m.stem) {
|
||||
return MatchPartial
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.HasPrefix(name, alias) {
|
||||
return MatchPartial
|
||||
}
|
||||
}
|
||||
if exactPath || lowerPath || strings.Contains(name, m.stem) {
|
||||
return MatchFuzzy
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.Contains(name, alias) {
|
||||
return MatchFuzzy
|
||||
}
|
||||
}
|
||||
return MatchNone
|
||||
}
|
||||
|
||||
// candidateName 保留同族内部名称的区分,只有无关联时使用调用方文件别名
|
||||
// 入参: file 字体文件候选
|
||||
// 返回: string 用于统一匹配名称与样式的规范名称
|
||||
func (m patternMatcher) candidateName(file FileCandidate) string {
|
||||
if len(file.names) == 0 || file.base == "" || m.stem == "" {
|
||||
return file.normalized
|
||||
}
|
||||
for _, name := range file.names {
|
||||
if strings.HasPrefix(name, m.stem) {
|
||||
return file.normalized
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.HasPrefix(name, alias) {
|
||||
return file.normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
return file.filename
|
||||
}
|
||||
|
||||
// styleSuffixNormalized 获取规范字体名称的样式后缀
|
||||
// 入参: name 规范字体名称
|
||||
// 返回: string 样式后缀
|
||||
func (m patternMatcher) styleSuffixNormalized(name string) string {
|
||||
if m.stem == "" || name == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(name, m.stem) {
|
||||
return strings.TrimPrefix(name, m.stem)
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.HasPrefix(name, alias) {
|
||||
return strings.TrimPrefix(name, alias)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// appendFileMatch 追加字体文件匹配结果
|
||||
// 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
|
||||
func appendFileMatch(matches *[]FileMatch, seen map[fileKey]int, matcher patternMatcher, file FileCandidate, rank int, bold, italic bool) {
|
||||
if rank == MatchNone {
|
||||
return
|
||||
}
|
||||
suffix := matcher.styleSuffixNormalized(matcher.candidateName(file))
|
||||
if rank == MatchFuzzy {
|
||||
suffix = matcher.styleSuffixNormalized(NormalizeName(file.base))
|
||||
}
|
||||
next := FileMatch{
|
||||
Name: file.Name,
|
||||
Face: file.Face,
|
||||
Priority: matcher.priority,
|
||||
Rank: rank,
|
||||
styleRank: fileStyleRank(suffix, bold, italic),
|
||||
sortName: file.lowerBase,
|
||||
}
|
||||
key := fileKey{file.fold, file.Face}
|
||||
if index, ok := seen[key]; ok {
|
||||
if fileMatchLess(next, (*matches)[index]) {
|
||||
(*matches)[index] = next
|
||||
}
|
||||
return
|
||||
}
|
||||
seen[key] = len(*matches)
|
||||
*matches = append(*matches, next)
|
||||
}
|
||||
|
||||
// sortFileMatches 排序字体文件匹配结果
|
||||
// 入参: matches 匹配结果
|
||||
func sortFileMatches(matches []FileMatch) {
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
return fileMatchLess(matches[i], matches[j])
|
||||
})
|
||||
}
|
||||
|
||||
// fileMatchLess 判断字体文件匹配结果优先级
|
||||
// 入参: left 左侧匹配结果, right 右侧匹配结果
|
||||
// 返回: bool 左侧是否优先
|
||||
func fileMatchLess(left, right FileMatch) bool {
|
||||
if left.Priority != right.Priority {
|
||||
return left.Priority < right.Priority
|
||||
}
|
||||
if left.Rank != right.Rank {
|
||||
return left.Rank < right.Rank
|
||||
}
|
||||
if left.styleRank != right.styleRank {
|
||||
return left.styleRank < right.styleRank
|
||||
}
|
||||
return left.sortName < right.sortName
|
||||
}
|
||||
|
||||
// FileMatchNames 获取字体文件匹配名称
|
||||
// 入参: matches 匹配结果
|
||||
// 返回: []string 字体文件列表
|
||||
func FileMatchNames(matches []FileMatch) []string {
|
||||
names := make([]string, 0, len(matches))
|
||||
seen := make(map[string]bool, len(matches))
|
||||
for _, match := range matches {
|
||||
key := strings.ToLower(match.Name)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
names = append(names, match.Name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// fileStyleRank 获取字体文件样式匹配等级
|
||||
// 入参: suffix 样式后缀, bold 是否粗体, italic 是否斜体
|
||||
// 返回: int 样式匹配等级
|
||||
func fileStyleRank(suffix string, bold, italic bool) int {
|
||||
fileBold, fileItalic := fileStyleFromSuffix(suffix)
|
||||
rank := 0
|
||||
if fileBold != bold {
|
||||
rank += 2
|
||||
}
|
||||
if fileItalic != italic {
|
||||
rank += 2
|
||||
}
|
||||
if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fileRegularSuffix(suffix) {
|
||||
rank++
|
||||
}
|
||||
return rank
|
||||
}
|
||||
|
||||
// fileStyleFromSuffix 获取样式后缀对应的字体样式
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否粗体, bool 是否斜体
|
||||
func fileStyleFromSuffix(suffix string) (bool, bool) {
|
||||
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
|
||||
}
|
||||
|
||||
// fileRegularSuffix 判断是否为常规样式后缀
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否为常规样式后缀
|
||||
func fileRegularSuffix(suffix string) bool {
|
||||
switch suffix {
|
||||
case "r", "regular", "normal", "常规":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// fileKnownStyleSuffix 判断是否为已知样式后缀
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否为已知样式后缀
|
||||
func fileKnownStyleSuffix(suffix string) bool {
|
||||
switch suffix {
|
||||
case "r", "regular", "normal", "常规",
|
||||
"b", "bd", "bold",
|
||||
"i", "it", "italic", "oblique",
|
||||
"bi", "bolditalic", "boldoblique":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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 geom
|
||||
|
||||
import "math"
|
||||
|
||||
// polygonPoint 允许同结构的后端顶点直接参与裁剪,避免整片坐标转换
|
||||
type polygonPoint interface {
|
||||
~struct{ X, Y float64 }
|
||||
}
|
||||
|
||||
// ClipPolygon 按非退化凸窗口逐边裁剪多边形,保留原顶点绕向,不修改输入
|
||||
// 入参: a 绘制顶点, b 已通过ConvexPolygon检查的裁剪顶点,均不含重复闭合点
|
||||
// 返回: []P 交集顶点,少于三个顶点时不构成区域, bool 是否满足数值精度
|
||||
func ClipPolygon[P polygonPoint](a, b []P) ([]P, bool) {
|
||||
for i, start := range b {
|
||||
origin, end := Point(start), Point(b[(i+1)%len(b)])
|
||||
dx, dy := end.X-origin.X, end.Y-origin.Y
|
||||
length := math.Hypot(dx, dy)
|
||||
if length == 0 {
|
||||
continue
|
||||
}
|
||||
dx, dy = dx/length, dy/length
|
||||
distance := func(p P) float64 { return convexDistance(origin, Point(p), dx, dy) }
|
||||
sign := 1.0
|
||||
for _, p := range b {
|
||||
if d := distance(p); d != 0 {
|
||||
sign = math.Copysign(1, d)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(a) == 0 {
|
||||
break
|
||||
}
|
||||
var clipped []P
|
||||
appendPoint := func(p P) {
|
||||
if len(clipped) == 0 || p != clipped[len(clipped)-1] {
|
||||
clipped = append(clipped, p)
|
||||
}
|
||||
}
|
||||
previous := a[len(a)-1]
|
||||
previousDistance := sign * distance(previous)
|
||||
if !finite(previousDistance) {
|
||||
return nil, false
|
||||
}
|
||||
for _, p := range a {
|
||||
d := sign * distance(p)
|
||||
if !finite(d) {
|
||||
return nil, false
|
||||
}
|
||||
if (d >= 0) != (previousDistance >= 0) {
|
||||
scale := math.Max(math.Abs(d), math.Abs(previousDistance))
|
||||
fraction := (previousDistance / scale) / (previousDistance/scale - d/scale)
|
||||
intersection := Lerp(Point(previous), Point(p), fraction)
|
||||
if dx == 0 {
|
||||
intersection.X = origin.X
|
||||
}
|
||||
if dy == 0 {
|
||||
intersection.Y = origin.Y
|
||||
}
|
||||
appendPoint(P(intersection))
|
||||
}
|
||||
if d >= 0 {
|
||||
appendPoint(p)
|
||||
}
|
||||
previous, previousDistance = p, d
|
||||
}
|
||||
if len(clipped) > 1 && clipped[0] == clipped[len(clipped)-1] {
|
||||
clipped = clipped[:len(clipped)-1]
|
||||
}
|
||||
a = clipped
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
|
||||
// ConvexPolygon 检查所有顶点位于各边的同一侧,不修改输入
|
||||
// 入参: points 不含重复闭合点的独立顶点
|
||||
// 返回: []P 原顶点,非有限距离或异侧顶点返回nil, bool 是否为非退化凸多边形
|
||||
func ConvexPolygon[P polygonPoint](points []P) ([]P, bool) {
|
||||
positive := false
|
||||
for i, start := range points {
|
||||
origin, end := Point(start), Point(points[(i+1)%len(points)])
|
||||
dx, dy := end.X-origin.X, end.Y-origin.Y
|
||||
length := math.Hypot(dx, dy)
|
||||
if !finite(length) {
|
||||
return nil, false
|
||||
}
|
||||
if length == 0 {
|
||||
continue
|
||||
}
|
||||
dx, dy = dx/length, dy/length
|
||||
sign := 0.0
|
||||
for _, p := range points {
|
||||
d := convexDistance(origin, Point(p), dx, dy)
|
||||
if !finite(d) || sign*d < 0 {
|
||||
return nil, false
|
||||
}
|
||||
if d != 0 {
|
||||
sign, positive = math.Copysign(1, d), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return points, positive
|
||||
}
|
||||
|
||||
// convexDistance 按浮点乘法误差界识别共线点,不使用固定坐标容差
|
||||
// 入参: start 边起点, point 待测顶点, dx 横向单位分量, dy 纵向单位分量
|
||||
// 返回: float64 顶点到边的有向距离
|
||||
func convexDistance(start, point Point, dx, dy float64) float64 {
|
||||
x, y := point.X-start.X, point.Y-start.Y
|
||||
distance := math.FMA(dx, y, -dy*x)
|
||||
errorBound := (math.Abs(dx*y) + math.Abs(dy*x)) * 0x1p-49
|
||||
if finite(errorBound) && math.Abs(distance) <= errorBound {
|
||||
return 0
|
||||
}
|
||||
return distance
|
||||
}
|
||||
@@ -20,8 +20,20 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"image"
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// PNGDensityChunkSize 为PNG像素密度块占用的字节数
|
||||
const PNGDensityChunkSize = 21
|
||||
|
||||
// Density 保存横纵像素密度,Unit为0表示比例、1表示每米像素数
|
||||
type Density struct {
|
||||
X, Y float64
|
||||
Unit byte
|
||||
}
|
||||
|
||||
// PageInfo 图片方向与像素密度,不改变原始编码
|
||||
type PageInfo struct {
|
||||
Orientation int
|
||||
@@ -232,3 +244,179 @@ func imageExifMetadata(data []byte, dpi float64, directory uint32) (int, float64
|
||||
}
|
||||
return orientation, x, y, nil
|
||||
}
|
||||
|
||||
// BMPDensity 提取已校验BMP头中的每米像素数
|
||||
// 入参: data 含至少40字节信息头的BMP编码
|
||||
// 返回: Density 像素密度, bool 密度是否可保留
|
||||
func BMPDensity(data []byte) (Density, bool) {
|
||||
xdpi, ydpi := int32(binary.LittleEndian.Uint32(data[38:42])), int32(binary.LittleEndian.Uint32(data[42:46]))
|
||||
if xdpi < 0 || ydpi < 0 {
|
||||
return Density{}, false
|
||||
}
|
||||
return Density{X: float64(xdpi), Y: float64(ydpi), Unit: 1}, true
|
||||
}
|
||||
|
||||
// TIFFConversionDensity 校验可转换的TIFF字段并提取规范化后的像素密度
|
||||
// 色彩配置、预乘透明度、扩展字段及其他目录需保留原始编码
|
||||
// 入参: data TIFF编码, layout 已校验的主图布局
|
||||
// 返回: Density PNG像素密度, bool 是否可完整转换
|
||||
func TIFFConversionDensity(data []byte, layout TIFFLayout) (Density, bool) {
|
||||
var density Density
|
||||
var order binary.ByteOrder = binary.LittleEndian
|
||||
if data[0] == 'M' {
|
||||
order = binary.BigEndian
|
||||
}
|
||||
if layout.Next != 0 || layout.Offset != order.Uint32(data[4:]) || layout.Reduced {
|
||||
return density, false
|
||||
}
|
||||
offset := int(layout.Offset)
|
||||
count := int(order.Uint16(data[offset:]))
|
||||
unit := uint32(2)
|
||||
var resolutions [2][2]uint32
|
||||
for i := range count {
|
||||
entry := data[offset+2+i*12 : offset+14+i*12]
|
||||
tag, kind, number := order.Uint16(entry), order.Uint16(entry[2:]), order.Uint32(entry[4:])
|
||||
value := order.Uint32(entry[8:])
|
||||
if kind == 3 && number == 1 {
|
||||
value = uint32(order.Uint16(entry[8:]))
|
||||
}
|
||||
switch tag {
|
||||
case 256, 257, 258, 259, 273, 274, 277, 278, 279, 320, 322, 323, 324, 325:
|
||||
case 262:
|
||||
if value == 5 {
|
||||
return density, false
|
||||
}
|
||||
case 254:
|
||||
if value != 0 {
|
||||
return density, false
|
||||
}
|
||||
case 255:
|
||||
if value != 1 {
|
||||
return density, false
|
||||
}
|
||||
case 266:
|
||||
if kind != 3 || number != 1 || value != 1 && value != 2 {
|
||||
return density, false
|
||||
}
|
||||
case 284:
|
||||
if kind != 3 || number != 1 || value != 1 && value != 2 {
|
||||
return density, false
|
||||
}
|
||||
case 292, 293:
|
||||
mask := uint32(7)
|
||||
if tag == 293 {
|
||||
mask = 2
|
||||
}
|
||||
if kind != 4 || number != 1 || value & ^mask != 0 {
|
||||
return density, false
|
||||
}
|
||||
case 317:
|
||||
if kind != 3 || number != 1 || value != 1 && value != 2 {
|
||||
return density, false
|
||||
}
|
||||
case 338:
|
||||
if kind != 3 || number != 1 || value != 2 {
|
||||
return density, false
|
||||
}
|
||||
case 339:
|
||||
if kind != 3 || number == 0 || number > 4 {
|
||||
return density, false
|
||||
}
|
||||
values := entry[8:]
|
||||
if number > 2 {
|
||||
if uint64(value)+uint64(number)*2 > uint64(len(data)) {
|
||||
return density, false
|
||||
}
|
||||
values = data[value:]
|
||||
}
|
||||
for j := uint32(0); j < number; j++ {
|
||||
if order.Uint16(values[j*2:]) != 1 {
|
||||
return density, false
|
||||
}
|
||||
}
|
||||
case 282, 283:
|
||||
if kind != 5 || number != 1 || uint64(value)+8 > uint64(len(data)) {
|
||||
return density, false
|
||||
}
|
||||
resolutions[tag-282] = [2]uint32{order.Uint32(data[value:]), order.Uint32(data[value+4:])}
|
||||
case 296:
|
||||
if kind != 3 || number != 1 || value < 1 || value > 3 {
|
||||
return density, false
|
||||
}
|
||||
unit = value
|
||||
default:
|
||||
return density, false
|
||||
}
|
||||
}
|
||||
if resolutions == [2][2]uint32{} {
|
||||
return density, true
|
||||
}
|
||||
x, y := resolutions[0], resolutions[1]
|
||||
if x[0] == 0 || x[1] == 0 || y[0] == 0 || y[1] == 0 {
|
||||
return density, false
|
||||
}
|
||||
if unit == 1 {
|
||||
a, b := uint64(x[0])*uint64(y[1]), uint64(y[0])*uint64(x[1])
|
||||
divisor, next := a, b
|
||||
for next != 0 {
|
||||
divisor, next = next, divisor%next
|
||||
}
|
||||
density.X, density.Y = float64(a/divisor), float64(b/divisor)
|
||||
} else {
|
||||
factor := 1 / 0.0254
|
||||
if unit == 3 {
|
||||
factor = 100
|
||||
}
|
||||
density = Density{X: float64(x[0]) / float64(x[1]) * factor, Y: float64(y[0]) / float64(y[1]) * factor, Unit: 1}
|
||||
}
|
||||
if math.Round(density.X) < 1 || math.Round(density.Y) < 1 || math.Round(density.X) > math.MaxInt32 || math.Round(density.Y) > math.MaxInt32 {
|
||||
return density, false
|
||||
}
|
||||
if layout.Orientation >= 5 {
|
||||
density.X, density.Y = density.Y, density.X
|
||||
}
|
||||
return density, true
|
||||
}
|
||||
|
||||
// Scale 按最终像素尺寸保留绝对密度或精确相对比例
|
||||
// 入参: before 原始像素尺寸, after 最终像素尺寸
|
||||
// 返回: Density 缩放后密度, bool 相对比例是否可完整保留
|
||||
func (density Density) Scale(before, after image.Point) (Density, bool) {
|
||||
hasDensity := density.X != 0 || density.Y != 0
|
||||
if hasDensity && before != after {
|
||||
if density.Unit == 0 {
|
||||
ratio := new(big.Rat).SetFrac64(int64(density.X)*int64(after.X), int64(density.Y)*int64(after.Y))
|
||||
ratio.Mul(ratio, new(big.Rat).SetFrac64(int64(before.Y), int64(before.X)))
|
||||
if ratio.Num().BitLen() > 31 || ratio.Denom().BitLen() > 31 {
|
||||
return density, false
|
||||
}
|
||||
density.X, density.Y = float64(ratio.Num().Int64()), float64(ratio.Denom().Int64())
|
||||
} else {
|
||||
density.X *= float64(after.X) / float64(before.X)
|
||||
density.Y *= float64(after.Y) / float64(before.Y)
|
||||
}
|
||||
}
|
||||
return density, true
|
||||
}
|
||||
|
||||
// WithPNGDensity 在无密度块的PNG头后插入pHYs,保留源编码不变
|
||||
// 入参: data 完整PNG编码, density 像素密度
|
||||
// 返回: []byte 带密度的编码, bool 密度是否可完整保留
|
||||
func WithPNGDensity(data []byte, density Density) ([]byte, bool) {
|
||||
x, y := math.Round(density.X), math.Round(density.Y)
|
||||
if x > math.MaxInt32 || y > math.MaxInt32 || density.X > 0 && x < 1 || density.Y > 0 && y < 1 {
|
||||
return nil, false
|
||||
}
|
||||
chunk := make([]byte, 21)
|
||||
binary.BigEndian.PutUint32(chunk, 9)
|
||||
copy(chunk[4:], "pHYs")
|
||||
binary.BigEndian.PutUint32(chunk[8:], uint32(x))
|
||||
binary.BigEndian.PutUint32(chunk[12:], uint32(y))
|
||||
chunk[16] = density.Unit
|
||||
binary.BigEndian.PutUint32(chunk[17:], crc32.ChecksumIEEE(chunk[4:17]))
|
||||
output := make([]byte, 0, len(data)+PNGDensityChunkSize)
|
||||
output = append(output, data[:33]...)
|
||||
output = append(output, chunk...)
|
||||
output = append(output, data[33:]...)
|
||||
return output, true
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/math/f64"
|
||||
)
|
||||
|
||||
// rasterScaleBufferLimit 限制整像素缩放器的浮点中间缓冲,超出时使用逐行采样
|
||||
const rasterScaleBufferLimit = 8 << 20
|
||||
|
||||
// rasterAxisSample 保存目标像素及其归一化源采样权重
|
||||
type rasterAxisSample struct {
|
||||
pixel int
|
||||
first int
|
||||
weights []float64
|
||||
}
|
||||
|
||||
// rasterImagePixel 保存未取整或裁切的预乘颜色累积值
|
||||
type rasterImagePixel struct {
|
||||
r, g, b, a float64
|
||||
}
|
||||
|
||||
// rasterImageRow 保存正在累积的目标行
|
||||
type rasterImageRow struct {
|
||||
sample rasterAxisSample
|
||||
values []rasterImagePixel
|
||||
}
|
||||
|
||||
// rasterImageSource 为通用图片提供预乘十六位采样
|
||||
type rasterImageSource struct{ image.Image }
|
||||
|
||||
// RGBA64At 读取通用颜色的原始预乘分量
|
||||
// 入参: x 横向坐标, y 纵向坐标
|
||||
// 返回: color.RGBA64 预乘颜色
|
||||
func (s rasterImageSource) RGBA64At(x, y int) color.RGBA64 {
|
||||
r, g, b, a := s.At(x, y).RGBA()
|
||||
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
|
||||
}
|
||||
|
||||
// drawRasterAxisImage 按像素中心进行轴向缩小,逐行复用横向采样并保留镜像与小数坐标
|
||||
// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作
|
||||
// 返回: bool 是否使用轴向采样
|
||||
func drawRasterAxisImage(dst draw.Image, src image.Image, m [6]float64, op draw.Op) bool {
|
||||
sx, sy := math.Abs(m[0]), math.Abs(m[3])
|
||||
if m[1] != 0 || m[2] != 0 || sx == 0 || sy == 0 || sx > 1 || sy > 1 || sx == 1 && sy == 1 {
|
||||
return false
|
||||
}
|
||||
for _, v := range m {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
b := src.Bounds()
|
||||
if b.Empty() {
|
||||
return true
|
||||
}
|
||||
r := TransformBounds(b, m, dst.Bounds())
|
||||
if r.Empty() {
|
||||
return true
|
||||
}
|
||||
xs := rasterImageAxis(r.Min.X, r.Max.X, b.Min.X, b.Max.X, m[0], m[4])
|
||||
ys := rasterImageAxis(r.Min.Y, r.Max.Y, b.Min.Y, b.Max.Y, m[3], m[5])
|
||||
if len(xs) == 0 || len(ys) == 0 {
|
||||
return true
|
||||
}
|
||||
if m[3] < 0 {
|
||||
slices.Reverse(ys)
|
||||
}
|
||||
pixels, ok := src.(image.RGBA64Image)
|
||||
if !ok {
|
||||
pixels = rasterImageSource{src}
|
||||
}
|
||||
width := len(xs)
|
||||
horizontal := make([]rasterImagePixel, width)
|
||||
columns := make([]int, width)
|
||||
output := image.NewRGBA64(image.Rect(0, 0, width, 1))
|
||||
var active []rasterImageRow
|
||||
var free [][]rasterImagePixel
|
||||
next := 0
|
||||
for y := ys[0].first; next < len(ys) || len(active) != 0; y++ {
|
||||
if len(active) == 0 && y < ys[next].first {
|
||||
y = ys[next].first
|
||||
}
|
||||
for next < len(ys) && ys[next].first == y {
|
||||
var row []rasterImagePixel
|
||||
if n := len(free); n != 0 {
|
||||
row, free = free[n-1], free[:n-1]
|
||||
clear(row)
|
||||
} else {
|
||||
row = make([]rasterImagePixel, width)
|
||||
}
|
||||
active = append(active, rasterImageRow{sample: ys[next], values: row})
|
||||
next++
|
||||
}
|
||||
rasterImageHorizontal(horizontal, pixels, xs, y, columns)
|
||||
n := 0
|
||||
for _, row := range active {
|
||||
weight := row.sample.weights[y-row.sample.first]
|
||||
if weight != 0 {
|
||||
for x, value := range horizontal {
|
||||
p := &row.values[x]
|
||||
p.r += value.r * weight
|
||||
p.g += value.g * weight
|
||||
p.b += value.b * weight
|
||||
p.a += value.a * weight
|
||||
}
|
||||
}
|
||||
if y+1 == row.sample.first+len(row.sample.weights) {
|
||||
rasterImageWriteRow(dst, output, row.values, xs[0].pixel, row.sample.pixel, op)
|
||||
free = append(free, row.values)
|
||||
} else {
|
||||
active[n] = row
|
||||
n++
|
||||
}
|
||||
}
|
||||
active = active[:n]
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// rasterImageAxis 按连续源坐标生成CatmullRom权重,不对图像几何边界取整
|
||||
// 入参: first 目标起点, end 目标终点, source 源起点, limit 源终点, scale 缩放比例, offset 平移距离
|
||||
// 返回: []rasterAxisSample 轴向采样表
|
||||
func rasterImageAxis(first, end, source, limit int, scale, offset float64) []rasterAxisSample {
|
||||
result := make([]rasterAxisSample, 0, end-first)
|
||||
argument := math.Abs(scale)
|
||||
support := draw.CatmullRom.Support / argument
|
||||
for pixel := first; pixel < end; pixel++ {
|
||||
center := (float64(pixel) + .5 - offset) / scale
|
||||
if center < float64(source) || center >= float64(limit) {
|
||||
continue
|
||||
}
|
||||
center -= .5
|
||||
start := int(max(float64(source), math.Floor(center-support)))
|
||||
stop := int(min(float64(limit), math.Ceil(center+support)))
|
||||
weights := make([]float64, stop-start)
|
||||
total := 0.0
|
||||
for i := range weights {
|
||||
distance := math.Abs((float64(start+i) - center) * argument)
|
||||
if distance < draw.CatmullRom.Support {
|
||||
weights[i] = draw.CatmullRom.At(distance)
|
||||
total += weights[i]
|
||||
}
|
||||
}
|
||||
for i := range weights {
|
||||
weights[i] /= total
|
||||
}
|
||||
result = append(result, rasterAxisSample{pixel: pixel, first: start, weights: weights})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// rasterImageHorizontal 按源列复用像素,保留各目标像素的权重累加顺序与中间精度
|
||||
// 入参: row 横向缓冲, src 源像素, samples 采样表, y 源行坐标, columns 活动列缓冲
|
||||
func rasterImageHorizontal(row []rasterImagePixel, src image.RGBA64Image, samples []rasterAxisSample, y int, columns []int) {
|
||||
clear(row)
|
||||
reversed := samples[0].first > samples[len(samples)-1].first
|
||||
start, end := samples[0].first, samples[0].first+len(samples[0].weights)
|
||||
for _, sample := range samples[1:] {
|
||||
start = min(start, sample.first)
|
||||
end = max(end, sample.first+len(sample.weights))
|
||||
}
|
||||
next, count := 0, 0
|
||||
for x := start; x < end; x++ {
|
||||
for next < len(samples) {
|
||||
index := next
|
||||
if reversed {
|
||||
index = len(samples) - next - 1
|
||||
}
|
||||
if samples[index].first > x {
|
||||
break
|
||||
}
|
||||
columns[count] = index
|
||||
count++
|
||||
next++
|
||||
}
|
||||
p := src.RGBA64At(x, y)
|
||||
n := 0
|
||||
for _, index := range columns[:count] {
|
||||
sample := samples[index]
|
||||
weight := sample.weights[x-sample.first]
|
||||
if weight != 0 {
|
||||
value := &row[index]
|
||||
value.r += float64(p.R) * weight
|
||||
value.g += float64(p.G) * weight
|
||||
value.b += float64(p.B) * weight
|
||||
value.a += float64(p.A) * weight
|
||||
}
|
||||
if x+1 < sample.first+len(sample.weights) {
|
||||
columns[n] = index
|
||||
n++
|
||||
}
|
||||
}
|
||||
count = n
|
||||
}
|
||||
}
|
||||
|
||||
// rasterImageWriteRow 在两轴采样完成后取整并使用标准预乘合成
|
||||
// 入参: dst 目标图像, output 行像素缓冲, values 累积颜色, x 横向起点, y 目标行坐标, op 合成操作
|
||||
func rasterImageWriteRow(dst draw.Image, output *image.RGBA64, values []rasterImagePixel, x, y int, op draw.Op) {
|
||||
for i, value := range values {
|
||||
output.SetRGBA64(i, 0, color.RGBA64{
|
||||
R: uint16(min(max(min(value.r, value.a)+.5, 0), 65535)),
|
||||
G: uint16(min(max(min(value.g, value.a)+.5, 0), 65535)),
|
||||
B: uint16(min(max(min(value.b, value.a)+.5, 0), 65535)),
|
||||
A: uint16(min(max(value.a+.5, 0), 65535)),
|
||||
})
|
||||
}
|
||||
draw.Draw(dst, image.Rect(x, y, x+len(values), y+1), output, image.Point{}, op)
|
||||
}
|
||||
|
||||
// DrawPixels 对轴向缩小使用可分离采样,其余变换保留通用路径
|
||||
// 入参: dst 目标图像, src 源图像, m 按a、b、c、d、e、f排列的像素变换, op 合成操作
|
||||
func DrawPixels(dst draw.Image, src image.Image, m [6]float64, op draw.Op) {
|
||||
b := src.Bounds()
|
||||
if dr, ok := rasterScaleBounds(b, m); ok {
|
||||
if dr.Intersect(dst.Bounds()).Empty() {
|
||||
return
|
||||
}
|
||||
draw.CatmullRom.Scale(dst, dr, src, b, op, nil)
|
||||
return
|
||||
}
|
||||
if drawRasterAxisImage(dst, src, m, op) {
|
||||
return
|
||||
}
|
||||
draw.CatmullRom.Transform(dst, f64.Aff3{m[0], m[2], m[4], m[1], m[3], m[5]}, src, b, op, nil)
|
||||
}
|
||||
|
||||
// rasterScaleBounds 检查变换是否可无几何取整地使用缩放器,并限制其中间缓冲
|
||||
// 入参: b 源边界, m 像素变换
|
||||
// 返回: image.Rectangle 缩放边界, bool 是否适用
|
||||
func rasterScaleBounds(b image.Rectangle, m [6]float64) (image.Rectangle, bool) {
|
||||
if b.Empty() || m[1] != 0 || m[2] != 0 || m[0] <= 0 || m[0] > 1 || m[3] <= 0 || m[3] > 1 || m[0] == 1 && m[3] == 1 {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
x0, y0 := float64(m[0]*float64(b.Min.X))+m[4], float64(m[3]*float64(b.Min.Y))+m[5]
|
||||
x1, y1 := float64(m[0]*float64(b.Max.X))+m[4], float64(m[3]*float64(b.Max.Y))+m[5]
|
||||
for _, v := range [4]float64{x0, y0, x1, y1} {
|
||||
if v != math.Floor(v) || v < math.MinInt32 || v > math.MaxInt32 {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
}
|
||||
if x1 <= x0 || y1 <= y0 || b.Dx() > math.MaxInt32 || b.Dy() > math.MaxInt32 || (x1-x0)*float64(b.Dy()) > rasterScaleBufferLimit/32 {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
return image.Rect(int(x0), int(y0), int(x1), int(y1)), true
|
||||
}
|
||||
|
||||
// TransformBounds 计算采样器可能写入的像素范围,并在整数转换前裁至目标边界
|
||||
// 入参: source 原始像素边界, m 像素变换, target 目标边界
|
||||
// 返回: image.Rectangle 受影响的目标像素区域
|
||||
func TransformBounds(source image.Rectangle, m [6]float64, target image.Rectangle) image.Rectangle {
|
||||
minX, minY, maxX, maxY := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
|
||||
for _, p := range [4]image.Point{source.Min, {X: source.Max.X, Y: source.Min.Y}, {X: source.Min.X, Y: source.Max.Y}, source.Max} {
|
||||
x := math.Floor(float64(m[0]*float64(p.X)) + float64(m[2]*float64(p.Y)) + m[4])
|
||||
y := math.Floor(float64(m[1]*float64(p.X)) + float64(m[3]*float64(p.Y)) + m[5])
|
||||
minX, minY = math.Min(minX, x), math.Min(minY, y)
|
||||
maxX, maxY = math.Max(maxX, x+1), math.Max(maxY, y+1)
|
||||
}
|
||||
minX, minY = math.Max(minX, float64(target.Min.X)), math.Max(minY, float64(target.Min.Y))
|
||||
maxX, maxY = math.Min(maxX, float64(target.Max.X)), math.Min(maxY, float64(target.Max.Y))
|
||||
if minX >= maxX || minY >= maxY {
|
||||
return image.Rectangle{}
|
||||
}
|
||||
return image.Rect(int(minX), int(minY), int(maxX), int(maxY))
|
||||
}
|
||||
Reference in new issue
Block a user