mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
d07972346b
commit
70003486b8
37 files changed
+2386
-2509
No files matched your search
@@ -15,13 +15,147 @@
|
|||||||
package font
|
package font
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
"strings"
|
"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 移除字体名称匹配时忽略的分隔符
|
// fontNameReplacer 移除字体名称匹配时忽略的分隔符
|
||||||
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
|
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 规范化字体名称
|
// NormalizeName 规范化字体名称
|
||||||
// 入参: name 字体名称
|
// 入参: name 字体名称
|
||||||
// 返回: string 规范化后的字体名称
|
// 返回: string 规范化后的字体名称
|
||||||
@@ -62,3 +196,611 @@ func AppendFontName(names []string, seen map[string]bool, name string) []string
|
|||||||
seen[key] = true
|
seen[key] = true
|
||||||
return append(names, name)
|
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"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PNGDensityChunkSize 为PNG像素密度块占用的字节数
|
||||||
|
const PNGDensityChunkSize = 21
|
||||||
|
|
||||||
|
// Density 保存横纵像素密度,Unit为0表示比例、1表示每米像素数
|
||||||
|
type Density struct {
|
||||||
|
X, Y float64
|
||||||
|
Unit byte
|
||||||
|
}
|
||||||
|
|
||||||
// PageInfo 图片方向与像素密度,不改变原始编码
|
// PageInfo 图片方向与像素密度,不改变原始编码
|
||||||
type PageInfo struct {
|
type PageInfo struct {
|
||||||
Orientation int
|
Orientation int
|
||||||
@@ -232,3 +244,179 @@ func imageExifMetadata(data []byte, dpi float64, directory uint32) (int, float64
|
|||||||
}
|
}
|
||||||
return orientation, x, y, nil
|
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
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package ofdgo
|
package image
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
@@ -21,8 +21,12 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"golang.org/x/image/draw"
|
"golang.org/x/image/draw"
|
||||||
|
"golang.org/x/image/math/f64"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// rasterScaleBufferLimit 限制整像素缩放器的浮点中间缓冲,超出时使用逐行采样
|
||||||
|
const rasterScaleBufferLimit = 8 << 20
|
||||||
|
|
||||||
// rasterAxisSample 保存目标像素及其归一化源采样权重
|
// rasterAxisSample 保存目标像素及其归一化源采样权重
|
||||||
type rasterAxisSample struct {
|
type rasterAxisSample struct {
|
||||||
pixel int
|
pixel int
|
||||||
@@ -55,7 +59,7 @@ func (s rasterImageSource) RGBA64At(x, y int) color.RGBA64 {
|
|||||||
// drawRasterAxisImage 按像素中心进行轴向缩小,逐行复用横向采样并保留镜像与小数坐标
|
// drawRasterAxisImage 按像素中心进行轴向缩小,逐行复用横向采样并保留镜像与小数坐标
|
||||||
// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作
|
// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作
|
||||||
// 返回: bool 是否使用轴向采样
|
// 返回: bool 是否使用轴向采样
|
||||||
func drawRasterAxisImage(dst draw.Image, src image.Image, m RasterMatrix, op draw.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])
|
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 {
|
if m[1] != 0 || m[2] != 0 || sx == 0 || sy == 0 || sx > 1 || sy > 1 || sx == 1 && sy == 1 {
|
||||||
return false
|
return false
|
||||||
@@ -69,7 +73,7 @@ func drawRasterAxisImage(dst draw.Image, src image.Image, m RasterMatrix, op dra
|
|||||||
if b.Empty() {
|
if b.Empty() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
r := rasterImageBounds(b, m, dst.Bounds())
|
r := TransformBounds(b, m, dst.Bounds())
|
||||||
if r.Empty() {
|
if r.Empty() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -223,3 +227,59 @@ func rasterImageWriteRow(dst draw.Image, output *image.RGBA64, values []rasterIm
|
|||||||
}
|
}
|
||||||
draw.Draw(dst, image.Rect(x, y, x+len(values), y+1), output, image.Point{}, op)
|
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))
|
||||||
|
}
|
||||||
@@ -15,11 +15,63 @@
|
|||||||
package pdf
|
package pdf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// compositeCancelInterval 保持像素合成的取消检查间隔
|
||||||
|
const compositeCancelInterval = 256
|
||||||
|
|
||||||
|
// CompositePixel 保存非预乘源空间分量及累计透明度
|
||||||
|
type CompositePixel struct {
|
||||||
|
Values [4]float64
|
||||||
|
Alpha float64
|
||||||
|
Effect float64
|
||||||
|
Shape float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// transferKey 区分图形状态、网屏覆盖函数及输出设备
|
||||||
|
type transferKey struct {
|
||||||
|
transfer *pdfgo.TransferFunction
|
||||||
|
halftone *pdfgo.Halftone
|
||||||
|
model pdfgo.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferCache 在页面内复用设备传递函数,零值可用
|
||||||
|
// 缓存与所属阅读器同生命周期,不用于并发解析
|
||||||
|
type TransferCache struct {
|
||||||
|
functions map[transferKey]*pdfgo.TransferFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve 在页面内复用输出设备的有效函数,不修改原始图形状态
|
||||||
|
// 入参: reader 原始阅读器, style 原始图形状态, model 原生设备颜色空间
|
||||||
|
// 返回: *pdfgo.TransferFunction 有效函数, error 网屏传递函数错误
|
||||||
|
func (c *TransferCache) Resolve(reader *pdfgo.Reader, style pdfgo.Style, model pdfgo.Name) (*pdfgo.TransferFunction, error) {
|
||||||
|
if style.Halftone == nil && style.Transfer == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if model == "" {
|
||||||
|
model = "DeviceRGB"
|
||||||
|
}
|
||||||
|
if c.functions == nil {
|
||||||
|
c.functions = make(map[transferKey]*pdfgo.TransferFunction)
|
||||||
|
}
|
||||||
|
key := transferKey{style.Transfer, style.Halftone, model}
|
||||||
|
transfer, found := c.functions[key]
|
||||||
|
if !found {
|
||||||
|
var err error
|
||||||
|
transfer, err = reader.DeviceTransfer(style.Transfer, style.Halftone, model)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.functions[key] = transfer
|
||||||
|
}
|
||||||
|
return transfer, nil
|
||||||
|
}
|
||||||
|
|
||||||
// OutputColor 将合成结果映射到OFD输出,与保留的设备四色对象使用相同显示换算
|
// OutputColor 将合成结果映射到OFD输出,与保留的设备四色对象使用相同显示换算
|
||||||
// 入参: space 合成空间, values 非预乘分量
|
// 入参: space 合成空间, values 非预乘分量
|
||||||
// 返回: [3]float64 显示颜色, error 校准颜色转换错误
|
// 返回: [3]float64 显示颜色, error 校准颜色转换错误
|
||||||
@@ -147,3 +199,139 @@ func CompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorSpac
|
|||||||
values, err := space.ConvertWith(values[:source.Components()], source, intent, conversion)
|
values, err := space.ConvertWith(values[:source.Components()], source, intent, conversion)
|
||||||
return values, true, err
|
return values, true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompositeInterpolate 按形状覆盖混合预乘颜色及累计透明度
|
||||||
|
// 入参: target 原像素与输出, source 替换像素, shape 覆盖率
|
||||||
|
// 返回: error 像素或覆盖率错误
|
||||||
|
func CompositeInterpolate(target *CompositePixel, source CompositePixel, shape float64) error {
|
||||||
|
pixel := pdfgo.ColorantPixel{Values: target.Values[:], Alpha: target.Alpha, Shape: target.Shape, Effect: target.Effect}
|
||||||
|
if err := pixel.Interpolate(pdfgo.ColorantPixel{Values: source.Values[:], Alpha: source.Alpha, Shape: source.Shape, Effect: source.Effect}, shape); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target.Alpha, target.Effect = pixel.Alpha, pixel.Effect
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositeKnockout 移除未覆盖区域的初始背景贡献,保留形状与不透明度差异
|
||||||
|
// 入参: target 累计组结果, initial 初始背景, source 单个对象合成结果
|
||||||
|
// 返回: error 像素错误
|
||||||
|
func CompositeKnockout(target *CompositePixel, initial, source CompositePixel) error {
|
||||||
|
pixel := pdfgo.ColorantPixel{Values: target.Values[:], Alpha: target.Alpha, Shape: target.Shape, Effect: target.Effect}
|
||||||
|
if err := pixel.Knockout(
|
||||||
|
pdfgo.ColorantPixel{Values: initial.Values[:], Alpha: initial.Alpha, Shape: initial.Shape, Effect: initial.Effect},
|
||||||
|
pdfgo.ColorantPixel{Values: source.Values[:], Alpha: source.Alpha, Shape: source.Shape, Effect: source.Effect},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target.Alpha, target.Effect, target.Shape = pixel.Alpha, pixel.Effect, pixel.Shape
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositeGroupRegion 在组覆盖区域合成像素,保留原栅格及形状贡献
|
||||||
|
// 入参: ctx 取消上下文, pixels 父组输出, initial 初始背景, result 组结果, stride 行跨度, region 覆盖区域, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
|
||||||
|
// 返回: error 缓冲、区域、颜色或混合错误
|
||||||
|
func CompositeGroupRegion(ctx context.Context, pixels, initial, result []CompositePixel, stride int, region image.Rectangle, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(pixels) != len(initial) || len(pixels) != len(result) || mask != nil && len(mask) != len(pixels) {
|
||||||
|
return fmt.Errorf("invalid PDF transparency group buffers")
|
||||||
|
}
|
||||||
|
if stride <= 0 || region.Min.X < 0 || region.Min.Y < 0 || region.Max.X > stride || !region.Empty() && region.Max.Y > len(pixels)/stride {
|
||||||
|
return fmt.Errorf("invalid PDF transparency group region")
|
||||||
|
}
|
||||||
|
group, destination := pdfgo.ColorantGroup{Space: space}, pdfgo.ColorantGroup{Space: parent}
|
||||||
|
compositor, err := destination.PrepareGroup(&group, mode, intent, conversion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if region.Empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
components, parentComponents := space.Components(), parent.Components()
|
||||||
|
var target pdfgo.ColorantPixel
|
||||||
|
for y := region.Min.Y; y < region.Max.Y; y++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, end := y*stride+region.Min.X, y*stride+region.Max.X; i < end; i++ {
|
||||||
|
if i%compositeCancelInterval == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alpha := opacity
|
||||||
|
if mask != nil {
|
||||||
|
alpha *= mask[i]
|
||||||
|
}
|
||||||
|
pixel := &result[i]
|
||||||
|
target.Values = pixels[i].Values[:parentComponents]
|
||||||
|
target.Alpha = pixels[i].Alpha
|
||||||
|
target.Shape = pixels[i].Shape
|
||||||
|
target.Effect = pixels[i].Effect
|
||||||
|
if err := compositor.Composite(&target,
|
||||||
|
pdfgo.ColorantPixel{Values: initial[i].Values[:components], Alpha: initial[i].Alpha, Shape: initial[i].Shape, Effect: initial[i].Effect},
|
||||||
|
pdfgo.ColorantPixel{Values: pixel.Values[:components], Alpha: pixel.Alpha, Shape: pixel.Shape, Effect: pixel.Effect},
|
||||||
|
alpha,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pixels[i].Alpha, pixels[i].Effect, pixels[i].Shape = target.Alpha, target.Effect, target.Shape
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositePaintOver 以兼容叠印计算零色料分量,再应用对象混合模式
|
||||||
|
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式, compatible 是否保留零色料背景, marked 原生过程通道,nil使用普通空间
|
||||||
|
// 返回: error 未支持的混合模式
|
||||||
|
func CompositePaintOver(backdrop *CompositePixel, source [4]float64, alpha float64, space *pdfgo.ColorSpace, mode pdfgo.Name, compatible bool, marked *[4]bool) error {
|
||||||
|
if !compatible && marked == nil {
|
||||||
|
return CompositeOver(backdrop, source, alpha, space, mode)
|
||||||
|
}
|
||||||
|
if alpha == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mask := PaintChannels(source, compatible, marked)
|
||||||
|
values, combined, err := space.CompositeOverprint(backdrop.Values[:space.Components()], source[:space.Components()], backdrop.Alpha, alpha, mode, mask[:space.Components()])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy(backdrop.Values[:space.Components()], values[:space.Components()])
|
||||||
|
backdrop.Alpha = combined
|
||||||
|
backdrop.Effect = alpha + backdrop.Effect*(1-alpha)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositeOver 按PDF透明模型合成源颜色,四色混合使用分量补色
|
||||||
|
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式
|
||||||
|
// 返回: error 未支持的混合模式
|
||||||
|
func CompositeOver(backdrop *CompositePixel, source [4]float64, alpha float64, space *pdfgo.ColorSpace, mode pdfgo.Name) error {
|
||||||
|
if alpha == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
values, combined, err := space.Composite(backdrop.Values[:space.Components()], source[:space.Components()], backdrop.Alpha, alpha, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy(backdrop.Values[:space.Components()], values[:space.Components()])
|
||||||
|
backdrop.Alpha = combined
|
||||||
|
backdrop.Effect = alpha + backdrop.Effect*(1-alpha)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaintChannels 选择兼容叠印实际使用源颜色的通道
|
||||||
|
// 入参: values 源四色分量, compatible 是否保留零分量背景, marked 指定通道,nil表示全部
|
||||||
|
// 返回: [4]bool 各通道是否取源颜色
|
||||||
|
func PaintChannels(values [4]float64, compatible bool, marked *[4]bool) [4]bool {
|
||||||
|
channels := [4]bool{true, true, true, true}
|
||||||
|
if marked != nil {
|
||||||
|
channels = *marked
|
||||||
|
}
|
||||||
|
if compatible {
|
||||||
|
for i := range channels {
|
||||||
|
channels[i] = channels[i] && values[i] != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channels
|
||||||
|
}
|
||||||
+91
-1
@@ -17,9 +17,10 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Action 动作
|
// Action 动作
|
||||||
@@ -548,3 +549,92 @@ func resolveActionURI(action URI) string {
|
|||||||
}
|
}
|
||||||
return base.ResolveReference(target).String()
|
return base.ResolveReference(target).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML 按标准属性写入目标,书签和动作采用相同格式
|
||||||
|
// 入参: encoder XML编码器, start 起始节点
|
||||||
|
// 返回: error 参数或编码错误
|
||||||
|
func (dest Dest) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error {
|
||||||
|
attrs, err := dest.attributes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
start.Attr = attrs
|
||||||
|
if err := encoder.EncodeToken(start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return encoder.EncodeToken(start.End())
|
||||||
|
}
|
||||||
|
|
||||||
|
// effective 应用省略属性的默认值,不改变原始声明
|
||||||
|
// 返回: Dest 生效的目标参数
|
||||||
|
func (dest Dest) effective() Dest {
|
||||||
|
if dest.OmitLeft {
|
||||||
|
dest.Left = 0
|
||||||
|
}
|
||||||
|
if dest.OmitTop {
|
||||||
|
dest.Top = 0
|
||||||
|
}
|
||||||
|
if dest.OmitZoom {
|
||||||
|
dest.Zoom = 0
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// attributes 校验并生成目标属性,缩放为0表示沿用当前比例
|
||||||
|
// 返回: ofdAttrs 标准属性, error 模式或参数错误
|
||||||
|
func (dest Dest) attributes() (ofdAttrs, error) {
|
||||||
|
dest = dest.effective()
|
||||||
|
attrs := ofdAttrs{{Name: xml.Name{Local: "Type"}, Value: dest.Type}, {Name: xml.Name{Local: "PageID"}, Value: dest.PageID}}
|
||||||
|
if dest.PageID == "" {
|
||||||
|
return nil, fmt.Errorf("missing destination page")
|
||||||
|
}
|
||||||
|
add := func(name string, value float64, omit bool) error {
|
||||||
|
if !finite(value) {
|
||||||
|
return fmt.Errorf("invalid destination %s", name)
|
||||||
|
}
|
||||||
|
if !omit {
|
||||||
|
attrs.add(name, ofdNumber(value))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch dest.Type {
|
||||||
|
case "XYZ":
|
||||||
|
if dest.Zoom != 0 && (dest.Zoom < 0.1 || dest.Zoom > 64) {
|
||||||
|
return nil, fmt.Errorf("destination zoom must be zero or between 0.1 and 64")
|
||||||
|
}
|
||||||
|
if err := add("Zoom", dest.Zoom, dest.OmitZoom); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case "FitR":
|
||||||
|
if err := add("Left", dest.Left, dest.OmitLeft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := add("Top", dest.Top, dest.OmitTop); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if dest.Type == "FitR" {
|
||||||
|
if dest.Right <= dest.Left || dest.Bottom <= dest.Top {
|
||||||
|
return nil, fmt.Errorf("empty destination rectangle")
|
||||||
|
}
|
||||||
|
if err := add("Right", dest.Right, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := add("Bottom", dest.Bottom, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "FitH":
|
||||||
|
if err := add("Top", dest.Top, dest.OmitTop); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "FitV":
|
||||||
|
if err := add("Left", dest.Left, dest.OmitLeft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "Fit":
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid destination type %q", dest.Type)
|
||||||
|
}
|
||||||
|
return attrs, nil
|
||||||
|
}
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MarshalXML 按标准属性写入目标,书签和动作采用相同格式
|
|
||||||
// 入参: encoder XML编码器, start 起始节点
|
|
||||||
// 返回: error 参数或编码错误
|
|
||||||
func (dest Dest) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error {
|
|
||||||
attrs, err := dest.attributes()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
start.Attr = attrs
|
|
||||||
if err := encoder.EncodeToken(start); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return encoder.EncodeToken(start.End())
|
|
||||||
}
|
|
||||||
|
|
||||||
// effective 应用省略属性的默认值,不改变原始声明
|
|
||||||
// 返回: Dest 生效的目标参数
|
|
||||||
func (dest Dest) effective() Dest {
|
|
||||||
if dest.OmitLeft {
|
|
||||||
dest.Left = 0
|
|
||||||
}
|
|
||||||
if dest.OmitTop {
|
|
||||||
dest.Top = 0
|
|
||||||
}
|
|
||||||
if dest.OmitZoom {
|
|
||||||
dest.Zoom = 0
|
|
||||||
}
|
|
||||||
return dest
|
|
||||||
}
|
|
||||||
|
|
||||||
// attributes 校验并生成目标属性,缩放为0表示沿用当前比例
|
|
||||||
// 返回: ofdAttrs 标准属性, error 模式或参数错误
|
|
||||||
func (dest Dest) attributes() (ofdAttrs, error) {
|
|
||||||
dest = dest.effective()
|
|
||||||
attrs := ofdAttrs{{Name: xml.Name{Local: "Type"}, Value: dest.Type}, {Name: xml.Name{Local: "PageID"}, Value: dest.PageID}}
|
|
||||||
if dest.PageID == "" {
|
|
||||||
return nil, fmt.Errorf("missing destination page")
|
|
||||||
}
|
|
||||||
add := func(name string, value float64, omit bool) error {
|
|
||||||
if !finite(value) {
|
|
||||||
return fmt.Errorf("invalid destination %s", name)
|
|
||||||
}
|
|
||||||
if !omit {
|
|
||||||
attrs.add(name, ofdNumber(value))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
switch dest.Type {
|
|
||||||
case "XYZ":
|
|
||||||
if dest.Zoom != 0 && (dest.Zoom < 0.1 || dest.Zoom > 64) {
|
|
||||||
return nil, fmt.Errorf("destination zoom must be zero or between 0.1 and 64")
|
|
||||||
}
|
|
||||||
if err := add("Zoom", dest.Zoom, dest.OmitZoom); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
case "FitR":
|
|
||||||
if err := add("Left", dest.Left, dest.OmitLeft); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := add("Top", dest.Top, dest.OmitTop); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if dest.Type == "FitR" {
|
|
||||||
if dest.Right <= dest.Left || dest.Bottom <= dest.Top {
|
|
||||||
return nil, fmt.Errorf("empty destination rectangle")
|
|
||||||
}
|
|
||||||
if err := add("Right", dest.Right, false); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := add("Bottom", dest.Bottom, false); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "FitH":
|
|
||||||
if err := add("Top", dest.Top, dest.OmitTop); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case "FitV":
|
|
||||||
if err := add("Left", dest.Left, dest.OmitLeft); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case "Fit":
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid destination type %q", dest.Type)
|
|
||||||
}
|
|
||||||
return attrs, nil
|
|
||||||
}
|
|
||||||
+40
-40
@@ -19,22 +19,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"hash/crc32"
|
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rasterDensity 保存横纵像素密度,unit为0表示比例、1表示每米像素数
|
|
||||||
type rasterDensity struct {
|
|
||||||
x, y float64
|
|
||||||
unit byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// outputImageFormat 识别可由资源压缩生成的图片格式
|
// outputImageFormat 识别可由资源压缩生成的图片格式
|
||||||
// 入参: data 图片编码
|
// 入参: data 图片编码
|
||||||
// 返回: string 标准资源格式,未识别时为空
|
// 返回: string 标准资源格式,未识别时为空
|
||||||
@@ -96,8 +87,8 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
|
|||||||
if int64(offset)+stride*int64(height) != int64(len(data)) {
|
if int64(offset)+stride*int64(height) != int64(len(data)) {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
xdpi, ydpi := int32(binary.LittleEndian.Uint32(data[38:42])), int32(binary.LittleEndian.Uint32(data[42:46]))
|
density, ok := baseimage.BMPDensity(data)
|
||||||
if xdpi < 0 || ydpi < 0 {
|
if !ok {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
if depth == 16 || depth == 32 {
|
if depth == 16 || depth == 32 {
|
||||||
@@ -122,7 +113,7 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return optimizeRasterResource(ctx, data, pixels, options, size, rasterDensity{x: float64(xdpi), y: float64(ydpi), unit: 1})
|
return optimizeRasterResource(ctx, data, pixels, options, size, density)
|
||||||
}
|
}
|
||||||
|
|
||||||
// optimizeRasterResource 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果
|
// optimizeRasterResource 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果
|
||||||
@@ -130,8 +121,8 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
|
|||||||
// 有密度时保留PNG,未知密度按最终像素选择编码格式
|
// 有密度时保留PNG,未知密度按最终像素选择编码格式
|
||||||
// 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度
|
// 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度
|
||||||
// 返回: []byte 图片编码, error 编码或取消错误
|
// 返回: []byte 图片编码, error 编码或取消错误
|
||||||
func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density rasterDensity) ([]byte, error) {
|
func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density baseimage.Density) ([]byte, error) {
|
||||||
hasDensity := density.x != 0 || density.y != 0
|
hasDensity := density.X != 0 || density.Y != 0
|
||||||
before := pixels.Bounds().Size()
|
before := pixels.Bounds().Size()
|
||||||
if options.Mode == CompressionLossy {
|
if options.Mode == CompressionLossy {
|
||||||
var err error
|
var err error
|
||||||
@@ -140,18 +131,10 @@ func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if after := pixels.Bounds().Size(); hasDensity && before != after {
|
var ok bool
|
||||||
if density.unit == 0 {
|
density, ok = density.Scale(before, pixels.Bounds().Size())
|
||||||
ratio := new(big.Rat).SetFrac64(int64(density.x)*int64(after.X), int64(density.y)*int64(after.Y))
|
if !ok {
|
||||||
ratio.Mul(ratio, new(big.Rat).SetFrac64(int64(before.Y), int64(before.X)))
|
return data, ctx.Err()
|
||||||
if ratio.Num().BitLen() > 31 || ratio.Denom().BitLen() > 31 {
|
|
||||||
return data, ctx.Err()
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var candidate []byte
|
var candidate []byte
|
||||||
var err error
|
var err error
|
||||||
@@ -169,23 +152,13 @@ func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if hasDensity {
|
if hasDensity {
|
||||||
x, y := math.Round(density.x), math.Round(density.y)
|
if len(candidate) > outputOptimizationBufferLimit-baseimage.PNGDensityChunkSize {
|
||||||
if x > math.MaxInt32 || y > math.MaxInt32 || density.x > 0 && x < 1 || density.y > 0 && y < 1 || len(candidate) > outputOptimizationBufferLimit-21 {
|
|
||||||
return data, ctx.Err()
|
return data, ctx.Err()
|
||||||
}
|
}
|
||||||
chunk := make([]byte, 21)
|
candidate, ok = baseimage.WithPNGDensity(candidate, density)
|
||||||
binary.BigEndian.PutUint32(chunk, 9)
|
if !ok {
|
||||||
copy(chunk[4:], "pHYs")
|
return data, ctx.Err()
|
||||||
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]))
|
|
||||||
var output bytes.Buffer
|
|
||||||
writer := imageMetadataWriter{writer: &output, skip: 33, data: chunk}
|
|
||||||
if _, err := writer.Write(candidate); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
candidate = output.Bytes()
|
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -195,3 +168,30 @@ func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image
|
|||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度
|
||||||
|
// 主图或单个瓦片的预计缓冲超限时保留原编码
|
||||||
|
// 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求
|
||||||
|
// 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误
|
||||||
|
func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) {
|
||||||
|
layout, err := baseimage.ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return data, ctx.Err()
|
||||||
|
}
|
||||||
|
pixelSize := int64(layout.DecodedPixelBytes())
|
||||||
|
if int64(layout.Width) > outputOptimizationBufferLimit/pixelSize/int64(layout.Height) {
|
||||||
|
return data, ctx.Err()
|
||||||
|
}
|
||||||
|
if layout.TileWidth != 0 && int64(layout.TileWidth) > outputOptimizationBufferLimit/pixelSize/int64(layout.TileHeight) {
|
||||||
|
return data, ctx.Err()
|
||||||
|
}
|
||||||
|
density, ok := baseimage.TIFFConversionDensity(data, layout)
|
||||||
|
if !ok {
|
||||||
|
return data, ctx.Err()
|
||||||
|
}
|
||||||
|
pixels, err := baseimage.DecodeTIFF(ctx, data, layout)
|
||||||
|
if err != nil {
|
||||||
|
return data, ctx.Err()
|
||||||
|
}
|
||||||
|
return optimizeRasterResource(ctx, data, pixels, options, size, density)
|
||||||
|
}
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"image"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
|
||||||
)
|
|
||||||
|
|
||||||
// optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度
|
|
||||||
// 主图或单个瓦片的预计缓冲超限时保留原编码
|
|
||||||
// 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求
|
|
||||||
// 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误
|
|
||||||
func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) {
|
|
||||||
layout, err := baseimage.ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
return data, ctx.Err()
|
|
||||||
}
|
|
||||||
pixelSize := int64(layout.DecodedPixelBytes())
|
|
||||||
if int64(layout.Width) > outputOptimizationBufferLimit/pixelSize/int64(layout.Height) {
|
|
||||||
return data, ctx.Err()
|
|
||||||
}
|
|
||||||
if layout.TileWidth != 0 && int64(layout.TileWidth) > outputOptimizationBufferLimit/pixelSize/int64(layout.TileHeight) {
|
|
||||||
return data, ctx.Err()
|
|
||||||
}
|
|
||||||
density, ok := tiffCompressionDensity(data, layout)
|
|
||||||
if !ok {
|
|
||||||
return data, ctx.Err()
|
|
||||||
}
|
|
||||||
pixels, err := baseimage.DecodeTIFF(ctx, data, layout)
|
|
||||||
if err != nil {
|
|
||||||
return data, ctx.Err()
|
|
||||||
}
|
|
||||||
return optimizeRasterResource(ctx, data, pixels, options, size, density)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tiffCompressionDensity 校验可转换的TIFF字段并提取规范化后的像素密度
|
|
||||||
// 色彩配置、预乘透明度、扩展字段及其他目录需保留原始编码
|
|
||||||
// 入参: data TIFF编码, layout 已校验的主图布局
|
|
||||||
// 返回: rasterDensity PNG像素密度, bool 是否可完整转换
|
|
||||||
func tiffCompressionDensity(data []byte, layout baseimage.TIFFLayout) (rasterDensity, bool) {
|
|
||||||
var density rasterDensity
|
|
||||||
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 = rasterDensity{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
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -258,3 +261,162 @@ func editorActionsXML(actions []Action) ([]byte, error) {
|
|||||||
node := root.child("Actions")
|
node := root.child("Actions")
|
||||||
return editorXMLStandalone(data[node.start:node.end], node)
|
return editorXMLStandalone(data[node.start:node.end], node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageActions 获取当前文档及版本中页面自身的完整动作独立副本,不合并模板或执行动作
|
||||||
|
// 入参: page 从0开始的页面索引
|
||||||
|
// 返回: []Action 原顺序动作,未设置时为nil, error 页面或解析错误
|
||||||
|
func (e *Editor) PageActions(page int) ([]Action, error) {
|
||||||
|
content, err := e.page(page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cloneEditorData(content.Actions), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectActions 获取普通页面图元自身的完整动作独立副本,不展开共享复合资源或执行动作
|
||||||
|
// 入参: page 从0开始的页面索引, id 与SetObjectActions相同的对象标识
|
||||||
|
// 返回: []Action 原顺序动作,未设置时为nil, error 页面、对象或类型错误
|
||||||
|
func (e *Editor) ObjectActions(page int, id string) ([]Action, error) {
|
||||||
|
layer, index, err := e.findObject(page, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
actions, err := resourceObjectActions(layer.Objects[index])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cloneEditorData(actions), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutlineActions 获取当前目录节点自身的完整动作独立副本,不含子目录或执行动作
|
||||||
|
// 入参: path 从0开始的非空层级索引
|
||||||
|
// 返回: []Action 原顺序动作,未设置时为nil, error 路径或解析错误
|
||||||
|
func (e *Editor) OutlineActions(path []int) ([]Action, error) {
|
||||||
|
if len(path) == 0 {
|
||||||
|
return nil, fmt.Errorf("outline path must not be empty")
|
||||||
|
}
|
||||||
|
data, err := e.outlineXML()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
node, err := editorOutlineNode(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
node = node.child("Actions")
|
||||||
|
if node == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
data, err = editorXMLStandalone(data[node.start:node.end], node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(data))
|
||||||
|
start, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var actions []Action
|
||||||
|
if err := decodeOFDActions(decoder, start.(xml.StartElement), &actions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return actions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateActionTargets 校验动作结构及当前文档、版本内的页面、书签、附件和音视频引用
|
||||||
|
// 可用于提交前检查,不改变现有Set方法允许悬空目标的语义;不检查事件归属或访问外部URI
|
||||||
|
// 已注册但未绑定的媒体也可引用;仅检查资源声明及类型,不解码媒体或执行动作
|
||||||
|
// 入参: ctx 取消上下文, actions 待校验动作列表
|
||||||
|
// 返回: error 首个错误及从0开始的动作索引,取消错误支持errors.Is
|
||||||
|
func (e *Editor) ValidateActionTargets(ctx context.Context, actions []Action) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var resources []ResourceInfo
|
||||||
|
resourcesLoaded := false
|
||||||
|
for index, action := range actions {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateActions([]Action{action}); err != nil {
|
||||||
|
return fmt.Errorf("action %d: %w", index, err)
|
||||||
|
}
|
||||||
|
if action.Goto != nil {
|
||||||
|
dest := action.Goto.Dest
|
||||||
|
if bookmark := action.Goto.Bookmark; bookmark != nil {
|
||||||
|
if e.source != nil {
|
||||||
|
for _, entry := range e.source.document.Bookmarks.Bookmark {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if entry.Name == bookmark.Name {
|
||||||
|
value := entry.Dest
|
||||||
|
dest = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dest == nil {
|
||||||
|
return fmt.Errorf("action %d: bookmark %q not found", index, bookmark.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := dest.attributes(); err != nil {
|
||||||
|
return fmt.Errorf("action %d: %w", index, err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, page := range e.pages {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sameResourceID(page.ID, dest.PageID) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("action %d: destination page %q not found", index, dest.PageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var kind, id string
|
||||||
|
switch {
|
||||||
|
case action.GotoA != nil:
|
||||||
|
kind, id = "Attachment", action.GotoA.AttachID
|
||||||
|
case action.Sound != nil:
|
||||||
|
kind, id = "Audio", action.Sound.ResourceID
|
||||||
|
case action.Movie != nil:
|
||||||
|
kind, id = "Video", action.Movie.ResourceID
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
registered := false
|
||||||
|
if kind != "Attachment" {
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if media := resource.image; media != nil && media.Type == kind && sameResourceID(media.ID, id) {
|
||||||
|
registered = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if registered {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !resourcesLoaded {
|
||||||
|
var err error
|
||||||
|
resources, err = e.Resources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("action %d: %w", index, err)
|
||||||
|
}
|
||||||
|
resourcesLoaded = true
|
||||||
|
}
|
||||||
|
if _, err := findResource(resources, kind, id); err != nil {
|
||||||
|
return fmt.Errorf("action %d: %w", index, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"context"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PageActions 获取当前文档及版本中页面自身的完整动作独立副本,不合并模板或执行动作
|
|
||||||
// 入参: page 从0开始的页面索引
|
|
||||||
// 返回: []Action 原顺序动作,未设置时为nil, error 页面或解析错误
|
|
||||||
func (e *Editor) PageActions(page int) ([]Action, error) {
|
|
||||||
content, err := e.page(page)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cloneEditorData(content.Actions), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectActions 获取普通页面图元自身的完整动作独立副本,不展开共享复合资源或执行动作
|
|
||||||
// 入参: page 从0开始的页面索引, id 与SetObjectActions相同的对象标识
|
|
||||||
// 返回: []Action 原顺序动作,未设置时为nil, error 页面、对象或类型错误
|
|
||||||
func (e *Editor) ObjectActions(page int, id string) ([]Action, error) {
|
|
||||||
layer, index, err := e.findObject(page, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
actions, err := resourceObjectActions(layer.Objects[index])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cloneEditorData(actions), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutlineActions 获取当前目录节点自身的完整动作独立副本,不含子目录或执行动作
|
|
||||||
// 入参: path 从0开始的非空层级索引
|
|
||||||
// 返回: []Action 原顺序动作,未设置时为nil, error 路径或解析错误
|
|
||||||
func (e *Editor) OutlineActions(path []int) ([]Action, error) {
|
|
||||||
if len(path) == 0 {
|
|
||||||
return nil, fmt.Errorf("outline path must not be empty")
|
|
||||||
}
|
|
||||||
data, err := e.outlineXML()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
root, err := parseEditorXML(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
node, err := editorOutlineNode(root, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
node = node.child("Actions")
|
|
||||||
if node == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
data, err = editorXMLStandalone(data[node.start:node.end], node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
decoder := xml.NewDecoder(bytes.NewReader(data))
|
|
||||||
start, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var actions []Action
|
|
||||||
if err := decodeOFDActions(decoder, start.(xml.StartElement), &actions); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return actions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateActionTargets 校验动作结构及当前文档、版本内的页面、书签、附件和音视频引用
|
|
||||||
// 可用于提交前检查,不改变现有Set方法允许悬空目标的语义;不检查事件归属或访问外部URI
|
|
||||||
// 已注册但未绑定的媒体也可引用;仅检查资源声明及类型,不解码媒体或执行动作
|
|
||||||
// 入参: ctx 取消上下文, actions 待校验动作列表
|
|
||||||
// 返回: error 首个错误及从0开始的动作索引,取消错误支持errors.Is
|
|
||||||
func (e *Editor) ValidateActionTargets(ctx context.Context, actions []Action) error {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var resources []ResourceInfo
|
|
||||||
resourcesLoaded := false
|
|
||||||
for index, action := range actions {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := validateActions([]Action{action}); err != nil {
|
|
||||||
return fmt.Errorf("action %d: %w", index, err)
|
|
||||||
}
|
|
||||||
if action.Goto != nil {
|
|
||||||
dest := action.Goto.Dest
|
|
||||||
if bookmark := action.Goto.Bookmark; bookmark != nil {
|
|
||||||
if e.source != nil {
|
|
||||||
for _, entry := range e.source.document.Bookmarks.Bookmark {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if entry.Name == bookmark.Name {
|
|
||||||
value := entry.Dest
|
|
||||||
dest = &value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dest == nil {
|
|
||||||
return fmt.Errorf("action %d: bookmark %q not found", index, bookmark.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := dest.attributes(); err != nil {
|
|
||||||
return fmt.Errorf("action %d: %w", index, err)
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for _, page := range e.pages {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if sameResourceID(page.ID, dest.PageID) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("action %d: destination page %q not found", index, dest.PageID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var kind, id string
|
|
||||||
switch {
|
|
||||||
case action.GotoA != nil:
|
|
||||||
kind, id = "Attachment", action.GotoA.AttachID
|
|
||||||
case action.Sound != nil:
|
|
||||||
kind, id = "Audio", action.Sound.ResourceID
|
|
||||||
case action.Movie != nil:
|
|
||||||
kind, id = "Video", action.Movie.ResourceID
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
registered := false
|
|
||||||
if kind != "Attachment" {
|
|
||||||
for _, resource := range e.resources {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if media := resource.image; media != nil && media.Type == kind && sameResourceID(media.ID, id) {
|
|
||||||
registered = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if registered {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !resourcesLoaded {
|
|
||||||
var err error
|
|
||||||
resources, err = e.Resources(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("action %d: %w", index, err)
|
|
||||||
}
|
|
||||||
resourcesLoaded = true
|
|
||||||
}
|
|
||||||
if _, err := findResource(resources, kind, id); err != nil {
|
|
||||||
return fmt.Errorf("action %d: %w", index, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
@@ -18,10 +18,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -366,3 +366,79 @@ func (e *Editor) collectCompositeIDs(node *editorXML, ids map[string]string) err
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Style 获取内部成员的独立样式快照,文字和路径CTM只表达页面描边倍率
|
||||||
|
// 可传入CopyStyle或CopyCompositeStyle,不作为几何复制快照
|
||||||
|
// 返回: GraphicObject 有效样式来源
|
||||||
|
func (m CompositeMember) Style() GraphicObject {
|
||||||
|
object := cloneEditorData(m.Object)
|
||||||
|
if object.Type == "PathObject" {
|
||||||
|
object.PathObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
||||||
|
} else if object.Type == "TextObject" {
|
||||||
|
object.TextObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyCompositeStyle 将同类型样式复制到内部选区,保留内容、位置、裁剪及其他实例
|
||||||
|
// 描边复制页面实际线宽和虚线,文字沿用目标段落和基线,全部成功后提交一次撤销
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, source Object或CompositeMember.Style取得的样式快照
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) CopyCompositeStyle(page int, path ObjectPath, indexes []int, source GraphicObject) error {
|
||||||
|
if err := e.validateCopyStyle(source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
source, err = e.explicitDefaultPaint(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, node := range nodes {
|
||||||
|
member := members[i]
|
||||||
|
if node.object.Type != source.Type || !member.Capabilities.Transform || source.Type != "ImageObject" && !member.Capabilities.Paint {
|
||||||
|
return fmt.Errorf("style requires editable members of the same type")
|
||||||
|
}
|
||||||
|
object := cloneEditorData(member.Object)
|
||||||
|
switch source.Type {
|
||||||
|
case "TextObject":
|
||||||
|
if err := e.updateCompositeText(node, member, nil, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size}, nil, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
object, err = e.compositeMemberStyle(node)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
style := copyEditorPathStyle(object.TextObject.pathStyle(), source.TextObject.pathStyle(), editorStrokeScale(source.TextObject.CTM)/member.StrokeScale)
|
||||||
|
if err := validateEditorStroke(style); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object.TextObject = object.TextObject.withPathStyle(style)
|
||||||
|
object.TextObject.DrawParam, err = e.neutralDrawParam()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "PathObject":
|
||||||
|
object.PathObject = copyEditorPathStyle(object.PathObject, source.PathObject, editorStrokeScale(source.PathObject.CTM)/member.StrokeScale)
|
||||||
|
if err := validateEditorStroke(object.PathObject); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
object.PathObject.DrawParam, err = e.neutralDrawParam()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "ImageObject":
|
||||||
|
object.ImageObject.Alpha = cloneEditorData(source.ImageObject.Alpha)
|
||||||
|
}
|
||||||
|
if err := node.update(object); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// 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 "fmt"
|
|
||||||
|
|
||||||
// Style 获取内部成员的独立样式快照,文字和路径CTM只表达页面描边倍率
|
|
||||||
// 可传入CopyStyle或CopyCompositeStyle,不作为几何复制快照
|
|
||||||
// 返回: GraphicObject 有效样式来源
|
|
||||||
func (m CompositeMember) Style() GraphicObject {
|
|
||||||
object := cloneEditorData(m.Object)
|
|
||||||
if object.Type == "PathObject" {
|
|
||||||
object.PathObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
|
||||||
} else if object.Type == "TextObject" {
|
|
||||||
object.TextObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
|
||||||
}
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyCompositeStyle 将同类型样式复制到内部选区,保留内容、位置、裁剪及其他实例
|
|
||||||
// 描边复制页面实际线宽和虚线,文字沿用目标段落和基线,全部成功后提交一次撤销
|
|
||||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, source Object或CompositeMember.Style取得的样式快照
|
|
||||||
// 返回: error 错误信息
|
|
||||||
func (e *Editor) CopyCompositeStyle(page int, path ObjectPath, indexes []int, source GraphicObject) error {
|
|
||||||
if err := e.validateCopyStyle(source); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
source, err = e.explicitDefaultPaint(source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i, node := range nodes {
|
|
||||||
member := members[i]
|
|
||||||
if node.object.Type != source.Type || !member.Capabilities.Transform || source.Type != "ImageObject" && !member.Capabilities.Paint {
|
|
||||||
return fmt.Errorf("style requires editable members of the same type")
|
|
||||||
}
|
|
||||||
object := cloneEditorData(member.Object)
|
|
||||||
switch source.Type {
|
|
||||||
case "TextObject":
|
|
||||||
if err := e.updateCompositeText(node, member, nil, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size}, nil, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
object, err = e.compositeMemberStyle(node)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
style := copyEditorPathStyle(object.TextObject.pathStyle(), source.TextObject.pathStyle(), editorStrokeScale(source.TextObject.CTM)/member.StrokeScale)
|
|
||||||
if err := validateEditorStroke(style); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
object.TextObject = object.TextObject.withPathStyle(style)
|
|
||||||
object.TextObject.DrawParam, err = e.neutralDrawParam()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "PathObject":
|
|
||||||
object.PathObject = copyEditorPathStyle(object.PathObject, source.PathObject, editorStrokeScale(source.PathObject.CTM)/member.StrokeScale)
|
|
||||||
if err := validateEditorStroke(object.PathObject); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
object.PathObject.DrawParam, err = e.neutralDrawParam()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "ImageObject":
|
|
||||||
object.ImageObject.Alpha = cloneEditorData(source.ImageObject.Alpha)
|
|
||||||
}
|
|
||||||
if err := node.update(object); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -275,7 +275,7 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
}
|
}
|
||||||
file = FontFile{Data: data}
|
file = FontFile{Data: data}
|
||||||
if bytes.HasPrefix(data, []byte("ttcf")) {
|
if bytes.HasPrefix(data, []byte("ttcf")) {
|
||||||
index = fontCollectionIndex(data, []string{declaration.font.FontName, declaration.font.FamilyName}, declaration.font.Bold, declaration.font.Italic)
|
index = basefont.CollectionIndex(data, []string{declaration.font.FontName, declaration.font.FamilyName}, declaration.font.Bold, declaration.font.Italic)
|
||||||
}
|
}
|
||||||
} else if len(file.Data) == 0 {
|
} else if len(file.Data) == 0 {
|
||||||
file, err = e.resolveBindingFont(ctx, scan, declaration)
|
file, err = e.resolveBindingFont(ctx, scan, declaration)
|
||||||
@@ -415,7 +415,7 @@ func originalBindingFont(ctx context.Context, scan *editorFontDocument, declarat
|
|||||||
}
|
}
|
||||||
index := 0
|
index := 0
|
||||||
if bytes.HasPrefix(data, []byte("ttcf")) {
|
if bytes.HasPrefix(data, []byte("ttcf")) {
|
||||||
index = fontCollectionIndex(data, []string{declaration.font.FontName, declaration.font.FamilyName}, declaration.font.Bold, declaration.font.Italic)
|
index = basefont.CollectionIndex(data, []string{declaration.font.FontName, declaration.font.FamilyName}, declaration.font.Bold, declaration.font.Italic)
|
||||||
}
|
}
|
||||||
data, err = (FontFile{Data: data}).Face(index)
|
data, err = (FontFile{Data: data}).Face(index)
|
||||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
||||||
@@ -67,3 +68,38 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) {
|
|||||||
}
|
}
|
||||||
return basefont.MissingGlyphs(data, text)
|
return basefont.MissingGlyphs(data, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FontNormalizeName 规范化字体名称
|
||||||
|
// 入参: name 字体名称
|
||||||
|
// 返回: string 规范化后的字体名称
|
||||||
|
func FontNormalizeName(name string) string {
|
||||||
|
return basefont.NormalizeName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontCandidateNames 获取字体候选名称
|
||||||
|
// 入参: names 字体名称列表
|
||||||
|
// 返回: []string 字体候选名称列表
|
||||||
|
func FontCandidateNames(names ...string) []string {
|
||||||
|
return basefont.CandidateNames(names...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontFilePatterns 获取字体文件匹配模式
|
||||||
|
// 入参: names 字体名称列表
|
||||||
|
// 返回: []string 字体文件匹配模式
|
||||||
|
func FontFilePatterns(names ...string) []string {
|
||||||
|
return basefont.FilePatterns(names...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontSystemNames 获取系统字体名称
|
||||||
|
// 入参: names 字体名称列表
|
||||||
|
// 返回: []string 系统字体名称
|
||||||
|
func FontSystemNames(names ...string) []string {
|
||||||
|
return basefont.SystemNames(names...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontFileMatches 匹配指定样式的字体文件名称
|
||||||
|
// 入参: files 可用字体文件名, bold 是否粗体, italic 是否斜体, names 字体名称,留空匹配默认回退字体
|
||||||
|
// 返回: []string 按优先级排列的匹配文件名
|
||||||
|
func FontFileMatches(files []string, bold, italic bool, names ...string) []string {
|
||||||
|
return basefont.FileMatchNames(basefont.FileMatches(basefont.FileCandidates(files, path.Base), names, bold, italic))
|
||||||
|
}
|
||||||
+12
-393
@@ -48,44 +48,7 @@ type FontFS struct {
|
|||||||
files map[string][]byte
|
files map[string][]byte
|
||||||
loaders map[string]func() ([]byte, error)
|
loaders map[string]func() ([]byte, error)
|
||||||
names []string
|
names []string
|
||||||
candidates []fontFileCandidate
|
candidates []basefont.FileCandidate
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileCandidate 字体文件候选
|
|
||||||
type fontFileCandidate struct {
|
|
||||||
name string
|
|
||||||
face int
|
|
||||||
base string
|
|
||||||
lowerBase string
|
|
||||||
filename string
|
|
||||||
normalized string
|
|
||||||
names []string
|
|
||||||
fold string
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontPatternMatcher 字体文件匹配器
|
|
||||||
type fontPatternMatcher struct {
|
|
||||||
pattern string
|
|
||||||
lowerPattern string
|
|
||||||
stem string
|
|
||||||
aliases []string
|
|
||||||
priority int
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileMatch 字体文件匹配结果
|
|
||||||
type fontFileMatch struct {
|
|
||||||
name string
|
|
||||||
face int
|
|
||||||
priority int
|
|
||||||
rank int
|
|
||||||
styleRank int
|
|
||||||
sortName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileKey 字体文件与集合索引的去重键
|
|
||||||
type fontFileKey struct {
|
|
||||||
name string
|
|
||||||
face int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fontMemFile 内存字体文件
|
// fontMemFile 内存字体文件
|
||||||
@@ -130,9 +93,9 @@ func NewFontFS(fonts []FontFile) *FontFS {
|
|||||||
fsys.files[name] = append([]byte(nil), font.Data...)
|
fsys.files[name] = append([]byte(nil), font.Data...)
|
||||||
}
|
}
|
||||||
sort.Strings(fsys.names)
|
sort.Strings(fsys.names)
|
||||||
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
|
fsys.candidates = basefont.FileCandidates(fsys.names, path.Base)
|
||||||
for i, file := range fsys.candidates {
|
for i, file := range fsys.candidates {
|
||||||
fsys.candidates = appendFontFileNames(fsys.candidates, i, basefont.FileNames(bytes.NewReader(fsys.files[file.name])))
|
fsys.candidates = basefont.AppendFileNames(fsys.candidates, i, basefont.FileNames(bytes.NewReader(fsys.files[file.Name])))
|
||||||
}
|
}
|
||||||
return fsys
|
return fsys
|
||||||
}
|
}
|
||||||
@@ -159,7 +122,7 @@ func NewLazyFontFS(names []string, load func(string) ([]byte, error)) *FontFS {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Strings(fsys.names)
|
sort.Strings(fsys.names)
|
||||||
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
|
fsys.candidates = basefont.FileCandidates(fsys.names, path.Base)
|
||||||
return fsys
|
return fsys
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,8 +201,8 @@ func (fsys *FontFS) Match(names ...string) (string, bool) {
|
|||||||
// 返回: string 匹配字体文件, bool 是否为名称匹配
|
// 返回: string 匹配字体文件, bool 是否为名称匹配
|
||||||
func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool) {
|
func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool) {
|
||||||
if len(names) > 0 {
|
if len(names) > 0 {
|
||||||
if matches := fsys.matchStyle(names, bold, italic); len(matches) > 0 {
|
if matches := basefont.FileMatches(fsys.candidates, names, bold, italic); len(matches) > 0 {
|
||||||
return matches[0].name, true
|
return matches[0].Name, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if matches := fsys.fallbackFonts(bold, italic); len(matches) > 0 {
|
if matches := fsys.fallbackFonts(bold, italic); len(matches) > 0 {
|
||||||
@@ -248,13 +211,6 @@ func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// FontFileMatches 匹配指定样式的字体文件名称
|
|
||||||
// 入参: files 可用字体文件名, bold 是否粗体, italic 是否斜体, names 字体名称,留空匹配默认回退字体
|
|
||||||
// 返回: []string 按优先级排列的匹配文件名
|
|
||||||
func FontFileMatches(files []string, bold, italic bool, names ...string) []string {
|
|
||||||
return fontFileMatchNames(fontFileMatches(fontFileCandidates(files, path.Base), names, bold, italic))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchFontFiles 按文档字体名称和样式选择首选文件,未声明的使用编号按默认规则回退
|
// MatchFontFiles 按文档字体名称和样式选择首选文件,未声明的使用编号按默认规则回退
|
||||||
// 入参: files 可用字体文件名, ids 已发现的使用编号,省略时仅匹配声明字体
|
// 入参: files 可用字体文件名, ids 已发现的使用编号,省略时仅匹配声明字体
|
||||||
// 返回: []string 所需字体文件名, error 错误信息
|
// 返回: []string 所需字体文件名, error 错误信息
|
||||||
@@ -268,19 +224,19 @@ func (r *Reader) MatchFontFiles(files []string, ids ...string) ([]string, error)
|
|||||||
fonts = append(fonts, Font{ID: id})
|
fonts = append(fonts, Font{ID: id})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
candidates := fontFileCandidates(files, path.Base)
|
candidates := basefont.FileCandidates(files, path.Base)
|
||||||
names := make([]string, 0, len(fonts))
|
names := make([]string, 0, len(fonts))
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for _, font := range fonts {
|
for _, font := range fonts {
|
||||||
if font.FontFile != "" {
|
if font.FontFile != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
matches := fontFileMatches(candidates, []string{font.FontName, font.FamilyName}, font.Bold, font.Italic)
|
matches := basefont.FileMatches(candidates, []string{font.FontName, font.FamilyName}, font.Bold, font.Italic)
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
matches = fontFileMatches(candidates, nil, font.Bold, font.Italic)
|
matches = basefont.FileMatches(candidates, nil, font.Bold, font.Italic)
|
||||||
}
|
}
|
||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
name := matches[0].name
|
name := matches[0].Name
|
||||||
if !seen[name] {
|
if !seen[name] {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
seen[name] = true
|
seen[name] = true
|
||||||
@@ -405,68 +361,12 @@ func (i fontFileInfo) Sys() any {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// matchStyle 匹配指定样式的字体名称
|
|
||||||
// 入参: names 字体名称列表, bold 是否粗体, italic 是否斜体
|
|
||||||
// 返回: []fontFileMatch 字体匹配列表
|
|
||||||
func (fsys *FontFS) matchStyle(names []string, bold, italic bool) []fontFileMatch {
|
|
||||||
return fontFileMatches(fsys.candidates, names, bold, italic)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileMatches 匹配字体文件候选
|
|
||||||
// 入参: candidates 字体候选, names 字体名称, bold 是否粗体, italic 是否斜体
|
|
||||||
// 返回: []fontFileMatch 按优先级排列的字体匹配
|
|
||||||
func fontFileMatches(candidates []fontFileCandidate, names []string, bold, italic bool) []fontFileMatch {
|
|
||||||
matches := make([]fontFileMatch, 0, len(candidates))
|
|
||||||
seen := make(map[fontFileKey]int, len(candidates))
|
|
||||||
if len(names) == 0 {
|
|
||||||
for _, name := range fontDefaultSystemNames() {
|
|
||||||
matches = append(matches, fontFileMatches(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 := fontFileKey{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}, fontQualifiedNames(name)...) {
|
|
||||||
matcher := fontPatternMatcher{stem: basefont.NormalizeName(alias), priority: (i-len(names))*2 + min(j, 1)}
|
|
||||||
if matcher.stem == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, file := range candidates {
|
|
||||||
if rank := matcher.rankCandidate(file); rank == fontMatchExact {
|
|
||||||
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, matcher := range newFontPatternMatchers(fontFilePatterns(names...)) {
|
|
||||||
for _, file := range candidates {
|
|
||||||
rank := matcher.rankCandidate(file)
|
|
||||||
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortFontFileMatches(matches)
|
|
||||||
return matches
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallbackFonts 获取回退字体文件
|
// fallbackFonts 获取回退字体文件
|
||||||
// 入参: bold 是否粗体, italic 是否斜体
|
// 入参: bold 是否粗体, italic 是否斜体
|
||||||
// 返回: []string 字体文件列表
|
// 返回: []string 字体文件列表
|
||||||
func (fsys *FontFS) fallbackFonts(bold, italic bool) []string {
|
func (fsys *FontFS) fallbackFonts(bold, italic bool) []string {
|
||||||
if matches := fsys.matchStyle(nil, bold, italic); len(matches) > 0 {
|
if matches := basefont.FileMatches(fsys.candidates, nil, bold, italic); len(matches) > 0 {
|
||||||
return fontFileMatchNames(matches)
|
return basefont.FileMatchNames(matches)
|
||||||
}
|
}
|
||||||
if len(fsys.names) > 0 {
|
if len(fsys.names) > 0 {
|
||||||
return []string{fsys.names[0]}
|
return []string{fsys.names[0]}
|
||||||
@@ -502,284 +402,3 @@ func cleanFontName(name string) string {
|
|||||||
}
|
}
|
||||||
return strings.ToLower(name)
|
return strings.ToLower(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fontFileCandidates 筛选字体文件候选
|
|
||||||
// 入参: names 文件名列表, base 文件名提取函数
|
|
||||||
// 返回: []fontFileCandidate 字体文件候选列表
|
|
||||||
func fontFileCandidates(names []string, base func(string) string) []fontFileCandidate {
|
|
||||||
files := make([]fontFileCandidate, 0, len(names))
|
|
||||||
for _, name := range names {
|
|
||||||
if basefont.IsFontFileName(name) {
|
|
||||||
baseName := base(name)
|
|
||||||
normalized := basefont.NormalizeName(strings.TrimSuffix(baseName, path.Ext(baseName)))
|
|
||||||
files = append(files, fontFileCandidate{
|
|
||||||
name: name,
|
|
||||||
base: baseName,
|
|
||||||
lowerBase: strings.ToLower(baseName),
|
|
||||||
filename: normalized,
|
|
||||||
normalized: normalized,
|
|
||||||
fold: strings.ToLower(name),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// newFontPatternMatcher 创建字体文件匹配器
|
|
||||||
// 入参: pattern 匹配模式
|
|
||||||
// 返回: fontPatternMatcher 字体文件匹配器
|
|
||||||
func newFontPatternMatcher(pattern string) fontPatternMatcher {
|
|
||||||
matcher := fontPatternMatcher{
|
|
||||||
pattern: pattern,
|
|
||||||
lowerPattern: strings.ToLower(pattern),
|
|
||||||
stem: fontPatternStem(pattern),
|
|
||||||
}
|
|
||||||
if matcher.stem != "" {
|
|
||||||
for _, alias := range fontExactCandidateNames(matcher.stem) {
|
|
||||||
alias = basefont.NormalizeName(alias)
|
|
||||||
if alias != "" {
|
|
||||||
matcher.aliases = append(matcher.aliases, alias)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matcher
|
|
||||||
}
|
|
||||||
|
|
||||||
// newFontPatternMatchers 创建字体文件匹配器列表
|
|
||||||
// 入参: patterns 匹配模式列表
|
|
||||||
// 返回: []fontPatternMatcher 字体文件匹配器列表
|
|
||||||
func newFontPatternMatchers(patterns []string) []fontPatternMatcher {
|
|
||||||
matchers := make([]fontPatternMatcher, 0, len(patterns))
|
|
||||||
for priority, pattern := range patterns {
|
|
||||||
matcher := newFontPatternMatcher(pattern)
|
|
||||||
matcher.priority = priority
|
|
||||||
matchers = append(matchers, matcher)
|
|
||||||
}
|
|
||||||
return matchers
|
|
||||||
}
|
|
||||||
|
|
||||||
// rank 获取字体文件匹配等级
|
|
||||||
// 入参: name 字体文件名
|
|
||||||
// 返回: int 匹配等级
|
|
||||||
func (m fontPatternMatcher) rank(name string) int {
|
|
||||||
return m.rankCandidate(fontFileCandidate{
|
|
||||||
base: name,
|
|
||||||
lowerBase: strings.ToLower(name),
|
|
||||||
normalized: basefont.NormalizeName(name),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// rankCandidate 获取字体文件候选匹配等级
|
|
||||||
// 入参: file 字体文件候选
|
|
||||||
// 返回: int 匹配等级
|
|
||||||
func (m fontPatternMatcher) rankCandidate(file fontFileCandidate) int {
|
|
||||||
exactPath, _ := path.Match(m.pattern, file.base)
|
|
||||||
lowerPath, _ := path.Match(m.lowerPattern, file.lowerBase)
|
|
||||||
if m.stem == "" {
|
|
||||||
if exactPath || lowerPath {
|
|
||||||
return fontMatchFuzzy
|
|
||||||
}
|
|
||||||
return fontMatchNone
|
|
||||||
}
|
|
||||||
name := m.candidateName(file)
|
|
||||||
if name == m.stem {
|
|
||||||
return fontMatchExact
|
|
||||||
}
|
|
||||||
for _, alias := range m.aliases {
|
|
||||||
if name == alias {
|
|
||||||
return fontMatchExact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fontFileKnownStyleSuffix(m.styleSuffixNormalized(name)) {
|
|
||||||
return fontMatchExact
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, m.stem) {
|
|
||||||
return fontMatchPartial
|
|
||||||
}
|
|
||||||
for _, alias := range m.aliases {
|
|
||||||
if strings.HasPrefix(name, alias) {
|
|
||||||
return fontMatchPartial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if exactPath || lowerPath || strings.Contains(name, m.stem) {
|
|
||||||
return fontMatchFuzzy
|
|
||||||
}
|
|
||||||
for _, alias := range m.aliases {
|
|
||||||
if strings.Contains(name, alias) {
|
|
||||||
return fontMatchFuzzy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fontMatchNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// candidateName 保留同族内部名称的区分,只有无关联时使用调用方文件别名
|
|
||||||
// 入参: file 字体文件候选
|
|
||||||
// 返回: string 用于统一匹配名称与样式的规范名称
|
|
||||||
func (m fontPatternMatcher) candidateName(file fontFileCandidate) 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 fontPatternMatcher) 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 ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// appendFontFileMatch 追加字体文件匹配结果
|
|
||||||
// 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
|
|
||||||
func appendFontFileMatch(matches *[]fontFileMatch, seen map[fontFileKey]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) {
|
|
||||||
if rank == fontMatchNone {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
suffix := matcher.styleSuffixNormalized(matcher.candidateName(file))
|
|
||||||
if rank == fontMatchFuzzy {
|
|
||||||
suffix = matcher.styleSuffixNormalized(basefont.NormalizeName(file.base))
|
|
||||||
}
|
|
||||||
next := fontFileMatch{
|
|
||||||
name: file.name,
|
|
||||||
face: file.face,
|
|
||||||
priority: matcher.priority,
|
|
||||||
rank: rank,
|
|
||||||
styleRank: fontFileStyleRank(suffix, bold, italic),
|
|
||||||
sortName: file.lowerBase,
|
|
||||||
}
|
|
||||||
key := fontFileKey{file.fold, file.face}
|
|
||||||
if index, ok := seen[key]; ok {
|
|
||||||
if fontFileMatchLess(next, (*matches)[index]) {
|
|
||||||
(*matches)[index] = next
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seen[key] = len(*matches)
|
|
||||||
*matches = append(*matches, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sortFontFileMatches 排序字体文件匹配结果
|
|
||||||
// 入参: matches 匹配结果
|
|
||||||
func sortFontFileMatches(matches []fontFileMatch) {
|
|
||||||
sort.SliceStable(matches, func(i, j int) bool {
|
|
||||||
return fontFileMatchLess(matches[i], matches[j])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileMatchLess 判断字体文件匹配结果优先级
|
|
||||||
// 入参: left 左侧匹配结果, right 右侧匹配结果
|
|
||||||
// 返回: bool 左侧是否优先
|
|
||||||
func fontFileMatchLess(left, right fontFileMatch) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileMatchNames 获取字体文件匹配名称
|
|
||||||
// 入参: matches 匹配结果
|
|
||||||
// 返回: []string 字体文件列表
|
|
||||||
func fontFileMatchNames(matches []fontFileMatch) []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
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileStyleRank 获取字体文件样式匹配等级
|
|
||||||
// 入参: suffix 样式后缀, bold 是否粗体, italic 是否斜体
|
|
||||||
// 返回: int 样式匹配等级
|
|
||||||
func fontFileStyleRank(suffix string, bold, italic bool) int {
|
|
||||||
fileBold, fileItalic := fontFileStyleFromSuffix(suffix)
|
|
||||||
rank := 0
|
|
||||||
if fileBold != bold {
|
|
||||||
rank += 2
|
|
||||||
}
|
|
||||||
if fileItalic != italic {
|
|
||||||
rank += 2
|
|
||||||
}
|
|
||||||
if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fontFileRegularSuffix(suffix) {
|
|
||||||
rank++
|
|
||||||
}
|
|
||||||
return rank
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileStyleFromSuffix 获取样式后缀对应的字体样式
|
|
||||||
// 入参: suffix 样式后缀
|
|
||||||
// 返回: bool 是否粗体, bool 是否斜体
|
|
||||||
func fontFileStyleFromSuffix(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileRegularSuffix 判断是否为常规样式后缀
|
|
||||||
// 入参: suffix 样式后缀
|
|
||||||
// 返回: bool 是否为常规样式后缀
|
|
||||||
func fontFileRegularSuffix(suffix string) bool {
|
|
||||||
switch suffix {
|
|
||||||
case "r", "regular", "normal", "常规":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontFileKnownStyleSuffix 判断是否为已知样式后缀
|
|
||||||
// 入参: suffix 样式后缀
|
|
||||||
// 返回: bool 是否为已知样式后缀
|
|
||||||
func fontFileKnownStyleSuffix(suffix string) bool {
|
|
||||||
switch suffix {
|
|
||||||
case "r", "regular", "normal", "常规",
|
|
||||||
"b", "bd", "bold",
|
|
||||||
"i", "it", "italic", "oblique",
|
|
||||||
"bi", "bolditalic", "boldoblique":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -129,7 +129,7 @@ func (r *Reader) fontData(id string, readonly bool) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if bytes.HasPrefix(data, []byte("ttcf")) {
|
if bytes.HasPrefix(data, []byte("ttcf")) {
|
||||||
index := fontCollectionIndex(data, []string{f.FontName, f.FamilyName}, f.Bold, f.Italic)
|
index := basefont.CollectionIndex(data, []string{f.FontName, f.FamilyName}, f.Bold, f.Italic)
|
||||||
return basefont.ExtractCollectionFont(data, index)
|
return basefont.ExtractCollectionFont(data, index)
|
||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
@@ -268,7 +268,7 @@ func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) {
|
|||||||
names[index] = face.Names
|
names[index] = face.Names
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
index := fontNameIndex(names, []string{of.FontName, of.FamilyName}, of.Bold, of.Italic)
|
index := basefont.NameIndex(names, []string{of.FontName, of.FamilyName}, of.Bold, of.Italic)
|
||||||
if faces[index] == nil {
|
if faces[index] == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,435 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
+30
-30
@@ -64,8 +64,8 @@ type fontSourceKey struct {
|
|||||||
// fontSourceCache 保存字体候选及独立字体数据,原始集合按字节预算淘汰,不包含绘图库对象
|
// fontSourceCache 保存字体候选及独立字体数据,原始集合按字节预算淘汰,不包含绘图库对象
|
||||||
type fontSourceCache struct {
|
type fontSourceCache struct {
|
||||||
candidates map[string][]fontSource
|
candidates map[string][]fontSource
|
||||||
directories map[string][]fontFileCandidate
|
directories map[string][]basefont.FileCandidate
|
||||||
filesystems map[int][]fontFileCandidate
|
filesystems map[int][]basefont.FileCandidate
|
||||||
data map[fontSourceKey]fontSourceData
|
data map[fontSourceKey]fontSourceData
|
||||||
collections *renderCache[fontSourceKey, []byte]
|
collections *renderCache[fontSourceKey, []byte]
|
||||||
fallback ResolvedFont
|
fallback ResolvedFont
|
||||||
@@ -81,7 +81,7 @@ type fontSourceData struct {
|
|||||||
// newFontSourceCache 创建独立字体来源缓存
|
// newFontSourceCache 创建独立字体来源缓存
|
||||||
// 返回: *fontSourceCache 字体来源缓存
|
// 返回: *fontSourceCache 字体来源缓存
|
||||||
func newFontSourceCache() *fontSourceCache {
|
func newFontSourceCache() *fontSourceCache {
|
||||||
return &fontSourceCache{candidates: make(map[string][]fontSource), directories: make(map[string][]fontFileCandidate), filesystems: make(map[int][]fontFileCandidate), data: make(map[fontSourceKey]fontSourceData), collections: &renderCache[fontSourceKey, []byte]{limit: 32 << 20}}
|
return &fontSourceCache{candidates: make(map[string][]fontSource), directories: make(map[string][]basefont.FileCandidate), filesystems: make(map[int][]basefont.FileCandidate), data: make(map[fontSourceKey]fontSourceData), collections: &renderCache[fontSourceKey, []byte]{limit: 32 << 20}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// child 复用相同来源的文件索引与数据,匹配和回退状态保持独立
|
// child 复用相同来源的文件索引与数据,匹配和回退状态保持独立
|
||||||
@@ -99,11 +99,11 @@ func (c *fontSourceCache) child() *fontSourceCache {
|
|||||||
func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) {
|
func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) {
|
||||||
key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name}
|
key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name}
|
||||||
if source.kind == fontSourceSystem {
|
if source.kind == fontSourceSystem {
|
||||||
matches := fontFileMatches(systemFontIndex(), []string{source.name}, definition.Bold, definition.Italic)
|
matches := basefont.FileMatches(systemFontIndex(), []string{source.name}, definition.Bold, definition.Italic)
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
return nil, fmt.Errorf("font %q not found", source.name)
|
return nil, fmt.Errorf("font %q not found", source.name)
|
||||||
}
|
}
|
||||||
key = fontSourceKey{kind: fontSourceFile, name: matches[0].name, face: matches[0].face}
|
key = fontSourceKey{kind: fontSourceFile, name: matches[0].Name, face: matches[0].Face}
|
||||||
}
|
}
|
||||||
if cached, ok := r.fontSourcesCache.data[key]; ok {
|
if cached, ok := r.fontSourcesCache.data[key]; ok {
|
||||||
return cached.data, cached.err
|
return cached.data, cached.err
|
||||||
@@ -192,7 +192,7 @@ func (r *Renderer) resolveFontSource(backend FontBackend, id string, exact bool)
|
|||||||
if !r.fontSourcesCache.fallbackRead {
|
if !r.fontSourcesCache.fallbackRead {
|
||||||
r.fontSourcesCache.fallbackRead = true
|
r.fontSourcesCache.fallbackRead = true
|
||||||
if canLoadSystemFonts() {
|
if canLoadSystemFonts() {
|
||||||
for _, name := range fontDefaultSystemNames() {
|
for _, name := range basefont.DefaultSystemNames() {
|
||||||
data, err := r.readFontSource(fontSource{kind: fontSourceSystem, name: name}, &Font{})
|
data, err := r.readFontSource(fontSource{kind: fontSourceSystem, name: name}, &Font{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
@@ -221,12 +221,12 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
|||||||
seen := make(map[fontSourceKey]bool)
|
seen := make(map[fontSourceKey]bool)
|
||||||
for _, dir := range r.fontDirs {
|
for _, dir := range r.fontDirs {
|
||||||
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
||||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.Name, face: match.Face, exact: match.Rank == basefont.MatchExact, priority: min(match.Priority, 0)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index := range r.fontFS {
|
for index := range r.fontFS {
|
||||||
for _, match := range r.matchFontFS(index, names, bold, italic) {
|
for _, match := range r.matchFontFS(index, names, bold, italic) {
|
||||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.Name, face: match.Face, exact: match.Rank == basefont.MatchExact, priority: min(match.Priority, 0)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sortFontSources(sources)
|
sortFontSources(sources)
|
||||||
@@ -234,10 +234,10 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
|||||||
if canLoadSystemFonts() {
|
if canLoadSystemFonts() {
|
||||||
for _, dir := range systemFontDirs() {
|
for _, dir := range systemFontDirs() {
|
||||||
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
||||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.Name, face: match.Face, exact: match.Rank == basefont.MatchExact, priority: min(match.Priority, 0)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, name := range fontSystemNames(names...) {
|
for _, name := range basefont.SystemNames(names...) {
|
||||||
source := fontSource{kind: fontSourceSystem, name: name, exact: true}
|
source := fontSource{kind: fontSourceSystem, name: name, exact: true}
|
||||||
for i, requested := range names {
|
for i, requested := range names {
|
||||||
if basefont.NormalizeName(name) == basefont.NormalizeName(requested) {
|
if basefont.NormalizeName(name) == basefont.NormalizeName(requested) {
|
||||||
@@ -251,12 +251,12 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
|||||||
sortFontSources(sources[systemStart:])
|
sortFontSources(sources[systemStart:])
|
||||||
for _, dir := range r.fontDirs {
|
for _, dir := range r.fontDirs {
|
||||||
for _, match := range r.matchFontFiles(dir, nil, bold, italic) {
|
for _, match := range r.matchFontFiles(dir, nil, bold, italic) {
|
||||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face})
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.Name, face: match.Face})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index := range r.fontFS {
|
for index := range r.fontFS {
|
||||||
for _, match := range r.matchFontFS(index, nil, bold, italic) {
|
for _, match := range r.matchFontFS(index, nil, bold, italic) {
|
||||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face})
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.Name, face: match.Face})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index, fsys := range r.fontFS {
|
for index, fsys := range r.fontFS {
|
||||||
@@ -294,39 +294,39 @@ func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source
|
|||||||
|
|
||||||
// matchFontFiles 查找字体文件
|
// matchFontFiles 查找字体文件
|
||||||
// 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体
|
// 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||||
// 返回: []fontFileMatch 字体匹配列表
|
// 返回: []basefont.FileMatch 字体匹配列表
|
||||||
func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []fontFileMatch {
|
func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []basefont.FileMatch {
|
||||||
candidates, ok := r.fontSourcesCache.directories[dir]
|
candidates, ok := r.fontSourcesCache.directories[dir]
|
||||||
if !ok {
|
if !ok {
|
||||||
files, _ := filepath.Glob(filepath.Join(dir, "*"))
|
files, _ := filepath.Glob(filepath.Join(dir, "*"))
|
||||||
candidates = fontFileCandidates(files, filepath.Base)
|
candidates = basefont.FileCandidates(files, filepath.Base)
|
||||||
for i, candidate := range candidates {
|
for i, candidate := range candidates {
|
||||||
file, err := os.Open(candidate.name)
|
file, err := os.Open(candidate.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
candidates = appendFontFileNames(candidates, i, basefont.FileNames(file))
|
candidates = basefont.AppendFileNames(candidates, i, basefont.FileNames(file))
|
||||||
file.Close()
|
file.Close()
|
||||||
}
|
}
|
||||||
r.fontSourcesCache.directories[dir] = candidates
|
r.fontSourcesCache.directories[dir] = candidates
|
||||||
}
|
}
|
||||||
return fontFileMatches(candidates, names, bold, italic)
|
return basefont.FileMatches(candidates, names, bold, italic)
|
||||||
}
|
}
|
||||||
|
|
||||||
// matchFontFS 匹配字体文件系统中的字体名称
|
// matchFontFS 匹配字体文件系统中的字体名称
|
||||||
// 入参: index 字体文件系统索引, names 字体名称, bold 是否粗体, italic 是否斜体
|
// 入参: index 字体文件系统索引, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||||
// 返回: []fontFileMatch 字体匹配列表
|
// 返回: []basefont.FileMatch 字体匹配列表
|
||||||
func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []fontFileMatch {
|
func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []basefont.FileMatch {
|
||||||
fsys := r.fontFS[index]
|
fsys := r.fontFS[index]
|
||||||
if fonts, ok := fsys.(*FontFS); ok {
|
if fonts, ok := fsys.(*FontFS); ok {
|
||||||
return fonts.matchStyle(names, bold, italic)
|
return basefont.FileMatches(fonts.candidates, names, bold, italic)
|
||||||
}
|
}
|
||||||
candidates, ok := r.fontSourcesCache.filesystems[index]
|
candidates, ok := r.fontSourcesCache.filesystems[index]
|
||||||
if !ok {
|
if !ok {
|
||||||
files, _ := fs.Glob(fsys, "*")
|
files, _ := fs.Glob(fsys, "*")
|
||||||
candidates = fontFileCandidates(files, path.Base)
|
candidates = basefont.FileCandidates(files, path.Base)
|
||||||
for i, candidate := range candidates {
|
for i, candidate := range candidates {
|
||||||
file, err := fsys.Open(candidate.name)
|
file, err := fsys.Open(candidate.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -335,12 +335,12 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f
|
|||||||
data, _ := io.ReadAll(file)
|
data, _ := io.ReadAll(file)
|
||||||
reader = bytes.NewReader(data)
|
reader = bytes.NewReader(data)
|
||||||
}
|
}
|
||||||
candidates = appendFontFileNames(candidates, i, basefont.FileNames(reader))
|
candidates = basefont.AppendFileNames(candidates, i, basefont.FileNames(reader))
|
||||||
file.Close()
|
file.Close()
|
||||||
}
|
}
|
||||||
r.fontSourcesCache.filesystems[index] = candidates
|
r.fontSourcesCache.filesystems[index] = candidates
|
||||||
}
|
}
|
||||||
return fontFileMatches(candidates, names, bold, italic)
|
return basefont.FileMatches(candidates, names, bold, italic)
|
||||||
}
|
}
|
||||||
|
|
||||||
// systemFontDirs 获取系统字体目录
|
// systemFontDirs 获取系统字体目录
|
||||||
@@ -378,8 +378,8 @@ func systemFontDirs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// indexSystemFonts 递归读取系统字体名称,保留集合索引
|
// indexSystemFonts 递归读取系统字体名称,保留集合索引
|
||||||
// 返回: []fontFileCandidate 字体候选
|
// 返回: []basefont.FileCandidate 字体候选
|
||||||
func indexSystemFonts() []fontFileCandidate {
|
func indexSystemFonts() []basefont.FileCandidate {
|
||||||
var files []string
|
var files []string
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for _, dir := range systemFontDirs() {
|
for _, dir := range systemFontDirs() {
|
||||||
@@ -391,13 +391,13 @@ func indexSystemFonts() []fontFileCandidate {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
candidates := fontFileCandidates(files, filepath.Base)
|
candidates := basefont.FileCandidates(files, filepath.Base)
|
||||||
for i, candidate := range candidates {
|
for i, candidate := range candidates {
|
||||||
file, err := os.Open(candidate.name)
|
file, err := os.Open(candidate.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
candidates = appendFontFileNames(candidates, i, basefont.FileNames(file))
|
candidates = basefont.AppendFileNames(candidates, i, basefont.FileNames(file))
|
||||||
file.Close()
|
file.Close()
|
||||||
}
|
}
|
||||||
return candidates
|
return candidates
|
||||||
|
|||||||
+132
@@ -23,6 +23,9 @@ import (
|
|||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// pdfObjectChunkLimit 限制导入暂存片段的容量,不限制页面对象数量
|
||||||
|
const pdfObjectChunkLimit = 256
|
||||||
|
|
||||||
// ErrPDFPassword 表示PDF需要正确的用户或所有者密码
|
// ErrPDFPassword 表示PDF需要正确的用户或所有者密码
|
||||||
var ErrPDFPassword = pdfgo.ErrPassword
|
var ErrPDFPassword = pdfgo.ErrPassword
|
||||||
|
|
||||||
@@ -118,6 +121,12 @@ type pdfImporter struct {
|
|||||||
compression CompressionOptions
|
compression CompressionOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfObjectBuffer 分块收集导入对象,提交前只整理一次连续数据
|
||||||
|
type pdfObjectBuffer struct {
|
||||||
|
parts [][]GraphicObject
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
||||||
// 入参: ctx 取消上下文, source PDF数据, size 字节数, options 转换选项
|
// 入参: ctx 取消上下文, source PDF数据, size 字节数, options 转换选项
|
||||||
// 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 错误信息
|
// 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 错误信息
|
||||||
@@ -321,3 +330,126 @@ func (p *pdfImporter) referencePage(ctx context.Context, source *pdfgo.Reader, r
|
|||||||
}
|
}
|
||||||
return p.references.Resolve(ctx, source, reference, p.resolveFile, p.resolveReference, p.warning)
|
return p.references.Resolve(ctx, source, reference, p.resolveFile, p.resolveReference, p.warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// objectValidation 在单次导入中共享不可变资源的校验结果,隔离局部编辑器
|
||||||
|
// 返回: *editorValidation 当前编辑器的构建校验会话
|
||||||
|
func (p *pdfImporter) objectValidation() *editorValidation {
|
||||||
|
if p.validation == nil || p.validation.Editor != p.editor {
|
||||||
|
p.validation = &editorValidation{Editor: p.editor}
|
||||||
|
}
|
||||||
|
return p.validation
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendObject 追加本次导入对象,按需建立独立暂存缓冲
|
||||||
|
// 入参: object 独立导入对象
|
||||||
|
func (p *pdfImporter) appendObject(object GraphicObject) {
|
||||||
|
if p.objects == nil {
|
||||||
|
p.objects = &pdfObjectBuffer{}
|
||||||
|
}
|
||||||
|
p.objects.append(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// append 追加对象,小批量连续存储,超过阈值后不复制前序片段
|
||||||
|
// 入参: object 独立对象
|
||||||
|
func (b *pdfObjectBuffer) append(object GraphicObject) {
|
||||||
|
index := len(b.parts) - 1
|
||||||
|
if index == 0 && len(b.parts[0]) == cap(b.parts[0]) && cap(b.parts[0]) < pdfObjectChunkLimit {
|
||||||
|
part := b.parts[0]
|
||||||
|
next := make([]GraphicObject, len(part), min(pdfObjectChunkLimit, max(1, cap(part)*2)))
|
||||||
|
copy(next, part)
|
||||||
|
b.parts[0] = next
|
||||||
|
}
|
||||||
|
if index < 0 || len(b.parts[index]) == cap(b.parts[index]) {
|
||||||
|
capacity := pdfObjectChunkLimit
|
||||||
|
if index < 0 {
|
||||||
|
capacity = 1
|
||||||
|
}
|
||||||
|
b.parts = append(b.parts, make([]GraphicObject, 0, capacity))
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
b.parts[index] = append(b.parts[index], object)
|
||||||
|
b.size++
|
||||||
|
}
|
||||||
|
|
||||||
|
// len 获取暂存对象数量,空缓冲返回零
|
||||||
|
// 返回: int 对象数量
|
||||||
|
func (b *pdfObjectBuffer) len() int {
|
||||||
|
if b == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncate 回滚到已有对象位置,清理丢弃对象及片段引用
|
||||||
|
// 入参: size 保留对象数量
|
||||||
|
func (b *pdfObjectBuffer) truncate(size int) {
|
||||||
|
if size < 0 || size > b.len() {
|
||||||
|
panic("invalid PDF object buffer size")
|
||||||
|
}
|
||||||
|
if size == b.len() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remaining := size
|
||||||
|
for index, part := range b.parts {
|
||||||
|
if remaining <= len(part) {
|
||||||
|
clear(part[remaining:])
|
||||||
|
b.parts[index] = part[:remaining]
|
||||||
|
clear(b.parts[index+1:])
|
||||||
|
b.parts = b.parts[:index+1]
|
||||||
|
b.size = size
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remaining -= len(part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// data 按绘制顺序整理独立对象数组,取消时保留原片段供重试
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: []GraphicObject 暂存数据, error 取消错误
|
||||||
|
func (b *pdfObjectBuffer) data(ctx context.Context) ([]GraphicObject, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if b.len() == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if len(b.parts) == 1 {
|
||||||
|
return b.parts[0][:b.size:b.size], nil
|
||||||
|
}
|
||||||
|
objects := make([]GraphicObject, b.size)
|
||||||
|
offset := 0
|
||||||
|
for _, part := range b.parts {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset += copy(objects[offset:], part)
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.parts[0] = objects
|
||||||
|
clear(b.parts[1:])
|
||||||
|
b.parts = b.parts[:1]
|
||||||
|
return objects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitObjects 将当前页面待转换对象写入编辑文档
|
||||||
|
// 返回: error 对象写入错误
|
||||||
|
func (p *pdfImporter) commitObjects() error {
|
||||||
|
if p.objects.len() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
objects, err := p.objects.data(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objects, err = p.sharedClipObjects(objects)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.objectValidation().appendOwnedObjects(p.ctx, p.page, objects); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.objects = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// pdfObjectChunkLimit 限制导入暂存片段的容量,不限制页面对象数量
|
|
||||||
const pdfObjectChunkLimit = 256
|
|
||||||
|
|
||||||
// pdfObjectBuffer 分块收集导入对象,提交前只整理一次连续数据
|
|
||||||
type pdfObjectBuffer struct {
|
|
||||||
parts [][]GraphicObject
|
|
||||||
size int
|
|
||||||
}
|
|
||||||
|
|
||||||
// objectValidation 在单次导入中共享不可变资源的校验结果,隔离局部编辑器
|
|
||||||
// 返回: *editorValidation 当前编辑器的构建校验会话
|
|
||||||
func (p *pdfImporter) objectValidation() *editorValidation {
|
|
||||||
if p.validation == nil || p.validation.Editor != p.editor {
|
|
||||||
p.validation = &editorValidation{Editor: p.editor}
|
|
||||||
}
|
|
||||||
return p.validation
|
|
||||||
}
|
|
||||||
|
|
||||||
// appendObject 追加本次导入对象,按需建立独立暂存缓冲
|
|
||||||
// 入参: object 独立导入对象
|
|
||||||
func (p *pdfImporter) appendObject(object GraphicObject) {
|
|
||||||
if p.objects == nil {
|
|
||||||
p.objects = &pdfObjectBuffer{}
|
|
||||||
}
|
|
||||||
p.objects.append(object)
|
|
||||||
}
|
|
||||||
|
|
||||||
// append 追加对象,小批量连续存储,超过阈值后不复制前序片段
|
|
||||||
// 入参: object 独立对象
|
|
||||||
func (b *pdfObjectBuffer) append(object GraphicObject) {
|
|
||||||
index := len(b.parts) - 1
|
|
||||||
if index == 0 && len(b.parts[0]) == cap(b.parts[0]) && cap(b.parts[0]) < pdfObjectChunkLimit {
|
|
||||||
part := b.parts[0]
|
|
||||||
next := make([]GraphicObject, len(part), min(pdfObjectChunkLimit, max(1, cap(part)*2)))
|
|
||||||
copy(next, part)
|
|
||||||
b.parts[0] = next
|
|
||||||
}
|
|
||||||
if index < 0 || len(b.parts[index]) == cap(b.parts[index]) {
|
|
||||||
capacity := pdfObjectChunkLimit
|
|
||||||
if index < 0 {
|
|
||||||
capacity = 1
|
|
||||||
}
|
|
||||||
b.parts = append(b.parts, make([]GraphicObject, 0, capacity))
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
b.parts[index] = append(b.parts[index], object)
|
|
||||||
b.size++
|
|
||||||
}
|
|
||||||
|
|
||||||
// len 获取暂存对象数量,空缓冲返回零
|
|
||||||
// 返回: int 对象数量
|
|
||||||
func (b *pdfObjectBuffer) len() int {
|
|
||||||
if b == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return b.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// truncate 回滚到已有对象位置,清理丢弃对象及片段引用
|
|
||||||
// 入参: size 保留对象数量
|
|
||||||
func (b *pdfObjectBuffer) truncate(size int) {
|
|
||||||
if size < 0 || size > b.len() {
|
|
||||||
panic("invalid PDF object buffer size")
|
|
||||||
}
|
|
||||||
if size == b.len() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
remaining := size
|
|
||||||
for index, part := range b.parts {
|
|
||||||
if remaining <= len(part) {
|
|
||||||
clear(part[remaining:])
|
|
||||||
b.parts[index] = part[:remaining]
|
|
||||||
clear(b.parts[index+1:])
|
|
||||||
b.parts = b.parts[:index+1]
|
|
||||||
b.size = size
|
|
||||||
return
|
|
||||||
}
|
|
||||||
remaining -= len(part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// data 按绘制顺序整理独立对象数组,取消时保留原片段供重试
|
|
||||||
// 入参: ctx 取消上下文
|
|
||||||
// 返回: []GraphicObject 暂存数据, error 取消错误
|
|
||||||
func (b *pdfObjectBuffer) data(ctx context.Context) ([]GraphicObject, error) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if b.len() == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if len(b.parts) == 1 {
|
|
||||||
return b.parts[0][:b.size:b.size], nil
|
|
||||||
}
|
|
||||||
objects := make([]GraphicObject, b.size)
|
|
||||||
offset := 0
|
|
||||||
for _, part := range b.parts {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
offset += copy(objects[offset:], part)
|
|
||||||
}
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b.parts[0] = objects
|
|
||||||
clear(b.parts[1:])
|
|
||||||
b.parts = b.parts[:1]
|
|
||||||
return objects, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// commitObjects 将当前页面待转换对象写入编辑文档
|
|
||||||
// 返回: error 对象写入错误
|
|
||||||
func (p *pdfImporter) commitObjects() error {
|
|
||||||
if p.objects.len() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
objects, err := p.objects.data(p.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
objects, err = p.sharedClipObjects(objects)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.objectValidation().appendOwnedObjects(p.ctx, p.page, objects); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.objects = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
+12
-17
@@ -38,13 +38,8 @@ type pdfCompositeNode struct {
|
|||||||
formCount int
|
formCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
|
// pdfCompositePixel 复用内部PDF像素布局
|
||||||
type pdfCompositePixel struct {
|
type pdfCompositePixel = codecpdf.CompositePixel
|
||||||
values [4]float64
|
|
||||||
alpha float64
|
|
||||||
effect float64
|
|
||||||
shape float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元
|
// pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元
|
||||||
type pdfCompositor struct {
|
type pdfCompositor struct {
|
||||||
@@ -121,7 +116,7 @@ type pdfCompositeCache struct {
|
|||||||
shadingPatterns map[*pdfgo.ShadingPattern][]pdfCompositeNode
|
shadingPatterns map[*pdfgo.ShadingPattern][]pdfCompositeNode
|
||||||
shadingSources map[pdfShadingSourceKey][]pdfCompositeNode
|
shadingSources map[pdfShadingSourceKey][]pdfCompositeNode
|
||||||
shadings map[pdfGradientKey]pdfCompositeGeometry
|
shadings map[pdfGradientKey]pdfCompositeGeometry
|
||||||
transfers map[pdfTransferKey]*pdfgo.TransferFunction
|
transfers codecpdf.TransferCache
|
||||||
textDisjoint map[*pdfgo.GroupMark]bool
|
textDisjoint map[*pdfgo.GroupMark]bool
|
||||||
groups map[*pdfgo.GroupMark]Box
|
groups map[*pdfgo.GroupMark]Box
|
||||||
}
|
}
|
||||||
@@ -420,7 +415,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg
|
|||||||
}
|
}
|
||||||
name := codecpdf.EmbeddedFontName(mark.Font.Name)
|
name := codecpdf.EmbeddedFontName(mark.Font.Name)
|
||||||
if !externalNames[basefont.NormalizeName(name)] {
|
if !externalNames[basefont.NormalizeName(name)] {
|
||||||
for _, candidate := range fontExactCandidateNames(name) {
|
for _, candidate := range basefont.ExactCandidateNames(name) {
|
||||||
externalNames[basefont.NormalizeName(candidate)] = true
|
externalNames[basefont.NormalizeName(candidate)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -907,7 +902,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i := range pixels {
|
for i := range pixels {
|
||||||
pixels[i].effect = 0
|
pixels[i].Effect = 0
|
||||||
}
|
}
|
||||||
copy(previousTransfers, c.transfers)
|
copy(previousTransfers, c.transfers)
|
||||||
if err := c.draw([]pdfCompositeNode{node}, pixels, space); err != nil {
|
if err := c.draw([]pdfCompositeNode{node}, pixels, space); err != nil {
|
||||||
@@ -919,16 +914,16 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if pixel.effect == 0 && (c.transfers == nil || c.transfers[i] == previousTransfers[i]) {
|
if pixel.Effect == 0 && (c.transfers == nil || c.transfers[i] == previousTransfers[i]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var rgb [3]float64
|
var rgb [3]float64
|
||||||
var err error
|
var err error
|
||||||
if c.transfers != nil && device == "DeviceCMYK" {
|
if c.transfers != nil && device == "DeviceCMYK" {
|
||||||
values := pixel.values
|
values := pixel.Values
|
||||||
if flatten {
|
if flatten {
|
||||||
for j := range values {
|
for j := range values {
|
||||||
values[j] *= pixel.alpha
|
values[j] *= pixel.Alpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
values, err = c.transfers[i].Apply(values[:], device, "")
|
values, err = c.transfers[i].Apply(values[:], device, "")
|
||||||
@@ -936,18 +931,18 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
|||||||
rgb, err = codecpdf.OutputColor(space, values)
|
rgb, err = codecpdf.OutputColor(space, values)
|
||||||
}
|
}
|
||||||
} else if c.transfers != nil {
|
} else if c.transfers != nil {
|
||||||
rgb, err = space.RGB(pixel.values[:space.Components()], "RelativeColorimetric")
|
rgb, err = space.RGB(pixel.Values[:space.Components()], "RelativeColorimetric")
|
||||||
} else {
|
} else {
|
||||||
rgb, err = codecpdf.OutputColor(space, pixel.values)
|
rgb, err = codecpdf.OutputColor(space, pixel.Values)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
alpha := pixel.alpha
|
alpha := pixel.Alpha
|
||||||
if flatten {
|
if flatten {
|
||||||
if c.transfers == nil || device != "DeviceCMYK" {
|
if c.transfers == nil || device != "DeviceCMYK" {
|
||||||
for j := range rgb {
|
for j := range rgb {
|
||||||
rgb[j] = pixel.alpha*rgb[j] + 1 - pixel.alpha
|
rgb[j] = pixel.Alpha*rgb[j] + 1 - pixel.Alpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
alpha = 1
|
alpha = 1
|
||||||
|
|||||||
+25
-147
@@ -15,9 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
@@ -84,8 +82,8 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
|
|||||||
defer c.releasePixels(result)
|
defer c.releasePixels(result)
|
||||||
copy(result, pixels)
|
copy(result, pixels)
|
||||||
for i := range result {
|
for i := range result {
|
||||||
result[i].effect = 0
|
result[i].Effect = 0
|
||||||
result[i].shape = 0
|
result[i].Shape = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +107,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if grouped {
|
if grouped {
|
||||||
return pdfCompositeGroupRegion(c.importer.ctx, pixels, initial, result, c.width, region, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion)
|
return codecpdf.CompositeGroupRegion(c.importer.ctx, pixels, initial, result, c.width, region, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -266,14 +264,14 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
if patternChannels != nil && patternChannels[index]&pdfTransferShape == 0 {
|
if patternChannels != nil && patternChannels[index]&pdfTransferShape == 0 {
|
||||||
selectionShape = 0
|
selectionShape = 0
|
||||||
}
|
}
|
||||||
values = sample.values
|
values = sample.Values
|
||||||
if paint.Shading != nil {
|
if paint.Shading != nil {
|
||||||
shape = sample.shape
|
shape = sample.Shape
|
||||||
} else {
|
} else {
|
||||||
shape *= sample.shape
|
shape *= sample.Shape
|
||||||
}
|
}
|
||||||
if sample.shape != 0 {
|
if sample.Shape != 0 {
|
||||||
opacity *= sample.alpha / sample.shape
|
opacity *= sample.Alpha / sample.Shape
|
||||||
}
|
}
|
||||||
if node.image != nil && paint.Shading == nil {
|
if node.image != nil && paint.Shading == nil {
|
||||||
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step})
|
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step})
|
||||||
@@ -359,22 +357,22 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
}
|
}
|
||||||
if initial != nil {
|
if initial != nil {
|
||||||
pixel := initial[index]
|
pixel := initial[index]
|
||||||
if err := pdfCompositePaintOver(&pixel, values, opacity, space, mode, compatible, marked); err != nil {
|
if err := codecpdf.CompositePaintOver(&pixel, values, opacity, space, mode, compatible, marked); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := pdfCompositeInterpolate(&pixels[index], pixel, shape); err != nil {
|
if err := codecpdf.CompositeInterpolate(&pixels[index], pixel, shape); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := pdfCompositePaintOver(&pixels[index], values, shape*opacity, space, mode, compatible, marked); err != nil {
|
if err := codecpdf.CompositePaintOver(&pixels[index], values, shape*opacity, space, mode, compatible, marked); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pixels[index].shape += shape * (1 - pixels[index].shape)
|
pixels[index].Shape += shape * (1 - pixels[index].Shape)
|
||||||
bits := pdfTransferChannels
|
bits := pdfTransferChannels
|
||||||
if (c.channels != nil || transfer != nil && c.transferModel == "DeviceCMYK") && (compatible || marked != nil || patternChannels != nil) {
|
if (c.channels != nil || transfer != nil && c.transferModel == "DeviceCMYK") && (compatible || marked != nil || patternChannels != nil) {
|
||||||
bits = 0
|
bits = 0
|
||||||
for j, selected := range pdfPaintChannels(values, compatible, marked) {
|
for j, selected := range codecpdf.PaintChannels(values, compatible, marked) {
|
||||||
if selected {
|
if selected {
|
||||||
bits |= 1 << j
|
bits |= 1 << j
|
||||||
}
|
}
|
||||||
@@ -409,18 +407,6 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositeInterpolate 按形状覆盖混合预乘颜色及累计透明度
|
|
||||||
// 入参: target 原像素与输出, source 替换像素, shape 覆盖率
|
|
||||||
// 返回: error 像素或覆盖率错误
|
|
||||||
func pdfCompositeInterpolate(target *pdfCompositePixel, source pdfCompositePixel, shape float64) error {
|
|
||||||
pixel := pdfgo.ColorantPixel{Values: target.values[:], Alpha: target.alpha, Shape: target.shape, Effect: target.effect}
|
|
||||||
if err := pixel.Interpolate(pdfgo.ColorantPixel{Values: source.values[:], Alpha: source.alpha, Shape: source.shape, Effect: source.effect}, shape); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
target.alpha, target.effect = pixel.Alpha, pixel.Effect
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawGroup 按隔离标志保留初始背景,移除背景贡献后应用组透明度
|
// drawGroup 按隔离标志保留初始背景,移除背景贡献后应用组透明度
|
||||||
// 非隔离同空间的非挖空组借用父组背景,子组只写入独立结果缓冲
|
// 非隔离同空间的非挖空组借用父组背景,子组只写入独立结果缓冲
|
||||||
// 入参: node 透明组, pixels 父组像素, parent 父混合空间
|
// 入参: node 透明组, pixels 父组像素, parent 父混合空间
|
||||||
@@ -481,7 +467,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pixel := &pixels[i]
|
pixel := &pixels[i]
|
||||||
values := pixel.values
|
values := pixel.Values
|
||||||
if !sameSpace {
|
if !sameSpace {
|
||||||
var err error
|
var err error
|
||||||
values, err = space.Convert(values[:components], parent, "RelativeColorimetric")
|
values, err = space.Convert(values[:components], parent, "RelativeColorimetric")
|
||||||
@@ -489,7 +475,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
background[i] = pdfCompositePixel{values: values, alpha: pixel.alpha}
|
background[i] = pdfCompositePixel{Values: values, Alpha: pixel.Alpha}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !sharedInitial {
|
if !sharedInitial {
|
||||||
@@ -526,13 +512,13 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result[i].shape *= g.Alpha
|
result[i].Shape *= g.Alpha
|
||||||
if mask != nil {
|
if mask != nil {
|
||||||
result[i].shape *= mask[i]
|
result[i].Shape *= mask[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := pdfCompositeGroupRegion(c.importer.ctx, pixels, initial, result, c.width, region, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion); err != nil {
|
if err := codecpdf.CompositeGroupRegion(c.importer.ctx, pixels, initial, result, c.width, region, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if c.channels != nil {
|
if c.channels != nil {
|
||||||
@@ -542,7 +528,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i, end := y*c.width+region.Min.X, y*c.width+region.Max.X; i < end; i++ {
|
for i, end := y*c.width+region.Min.X, y*c.width+region.Max.X; i < end; i++ {
|
||||||
if result[i].shape != 0 && local.channels[i]&pdfTransferShape != 0 {
|
if result[i].Shape != 0 && local.channels[i]&pdfTransferShape != 0 {
|
||||||
c.channels[i] = local.channels[i]
|
c.channels[i] = local.channels[i]
|
||||||
if independent {
|
if independent {
|
||||||
c.channels[i] |= pdfTransferChannels
|
c.channels[i] |= pdfTransferChannels
|
||||||
@@ -605,10 +591,10 @@ func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels [
|
|||||||
for y := region.Min.Y; y < region.Max.Y; y++ {
|
for y := region.Min.Y; y < region.Max.Y; y++ {
|
||||||
for x := region.Min.X; x < region.Max.X; x++ {
|
for x := region.Min.X; x < region.Max.X; x++ {
|
||||||
i := y*c.width + x
|
i := y*c.width + x
|
||||||
if err := pdfCompositeKnockout(&pixels[i], initial[i], candidate[i]); err != nil {
|
if err := codecpdf.CompositeKnockout(&pixels[i], initial[i], candidate[i]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if c.channels != nil && candidate[i].shape != 0 {
|
if c.channels != nil && candidate[i].Shape != 0 {
|
||||||
c.channels[i] = local.channels[i]
|
c.channels[i] = local.channels[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,76 +604,6 @@ func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels [
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositeKnockout 移除未覆盖区域的初始背景贡献,保留形状与不透明度差异
|
|
||||||
// 入参: target 累计组结果, initial 初始背景, source 单个对象合成结果
|
|
||||||
// 返回: error 像素错误
|
|
||||||
func pdfCompositeKnockout(target *pdfCompositePixel, initial, source pdfCompositePixel) error {
|
|
||||||
pixel := pdfgo.ColorantPixel{Values: target.values[:], Alpha: target.alpha, Shape: target.shape, Effect: target.effect}
|
|
||||||
if err := pixel.Knockout(
|
|
||||||
pdfgo.ColorantPixel{Values: initial.values[:], Alpha: initial.alpha, Shape: initial.shape, Effect: initial.effect},
|
|
||||||
pdfgo.ColorantPixel{Values: source.values[:], Alpha: source.alpha, Shape: source.shape, Effect: source.effect},
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
target.alpha, target.effect, target.shape = pixel.Alpha, pixel.Effect, pixel.Shape
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfCompositeGroupRegion 在组覆盖区域合成像素,保留原栅格及形状贡献
|
|
||||||
// 入参: ctx 取消上下文, pixels 父组输出, initial 初始背景, result 组结果, stride 行跨度, region 覆盖区域, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
|
|
||||||
// 返回: error 缓冲、区域、颜色或混合错误
|
|
||||||
func pdfCompositeGroupRegion(ctx context.Context, pixels, initial, result []pdfCompositePixel, stride int, region image.Rectangle, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(pixels) != len(initial) || len(pixels) != len(result) || mask != nil && len(mask) != len(pixels) {
|
|
||||||
return fmt.Errorf("invalid PDF transparency group buffers")
|
|
||||||
}
|
|
||||||
if stride <= 0 || region.Min.X < 0 || region.Min.Y < 0 || region.Max.X > stride || !region.Empty() && region.Max.Y > len(pixels)/stride {
|
|
||||||
return fmt.Errorf("invalid PDF transparency group region")
|
|
||||||
}
|
|
||||||
group, destination := pdfgo.ColorantGroup{Space: space}, pdfgo.ColorantGroup{Space: parent}
|
|
||||||
compositor, err := destination.PrepareGroup(&group, mode, intent, conversion)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if region.Empty() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
components, parentComponents := space.Components(), parent.Components()
|
|
||||||
var target pdfgo.ColorantPixel
|
|
||||||
for y := region.Min.Y; y < region.Max.Y; y++ {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i, end := y*stride+region.Min.X, y*stride+region.Max.X; i < end; i++ {
|
|
||||||
if i%pdfCompositeTileSize == 0 {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
alpha := opacity
|
|
||||||
if mask != nil {
|
|
||||||
alpha *= mask[i]
|
|
||||||
}
|
|
||||||
pixel := &result[i]
|
|
||||||
target.Values = pixels[i].values[:parentComponents]
|
|
||||||
target.Alpha = pixels[i].alpha
|
|
||||||
target.Shape = pixels[i].shape
|
|
||||||
target.Effect = pixels[i].effect
|
|
||||||
if err := compositor.Composite(&target,
|
|
||||||
pdfgo.ColorantPixel{Values: initial[i].values[:components], Alpha: initial[i].alpha, Shape: initial[i].shape, Effect: initial[i].effect},
|
|
||||||
pdfgo.ColorantPixel{Values: pixel.values[:components], Alpha: pixel.alpha, Shape: pixel.shape, Effect: pixel.effect},
|
|
||||||
alpha,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pixels[i].alpha, pixels[i].effect, pixels[i].shape = target.Alpha, target.Effect, target.Shape
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mask 在组颜色空间计算蒙版,保留背景色、组隔离和亮度传递
|
// mask 在组颜色空间计算蒙版,保留背景色、组隔离和亮度传递
|
||||||
// 入参: mask 蒙版或空值, inherited 未指定蒙版空间时继承的空间
|
// 入参: mask 蒙版或空值, inherited 未指定蒙版空间时继承的空间
|
||||||
// 返回: []float64 蒙版透明度或空值, error 蒙版错误
|
// 返回: []float64 蒙版透明度或空值, error 蒙版错误
|
||||||
@@ -724,8 +640,8 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
copy(pixels[i].values[:], mask.Backdrop)
|
copy(pixels[i].Values[:], mask.Backdrop)
|
||||||
pixels[i].alpha = 1
|
pixels[i].Alpha = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
local := *c
|
local := *c
|
||||||
@@ -748,9 +664,9 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
v := pixel.alpha
|
v := pixel.Alpha
|
||||||
if mask.Subtype == "Luminosity" {
|
if mask.Subtype == "Luminosity" {
|
||||||
v, err = space.Luminosity(pixel.values[:space.Components()], "RelativeColorimetric")
|
v, err = space.Luminosity(pixel.Values[:space.Components()], "RelativeColorimetric")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -917,41 +833,3 @@ func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, e
|
|||||||
}
|
}
|
||||||
return values, visible, err
|
return values, visible, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositePaintOver 以兼容叠印计算零色料分量,再应用对象混合模式
|
|
||||||
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式, compatible 是否保留零色料背景, marked 原生过程通道,nil使用普通空间
|
|
||||||
// 返回: error 未支持的混合模式
|
|
||||||
func pdfCompositePaintOver(backdrop *pdfCompositePixel, source [4]float64, alpha float64, space *pdfgo.ColorSpace, mode pdfgo.Name, compatible bool, marked *[4]bool) error {
|
|
||||||
if !compatible && marked == nil {
|
|
||||||
return pdfCompositeOver(backdrop, source, alpha, space, mode)
|
|
||||||
}
|
|
||||||
if alpha == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
mask := pdfPaintChannels(source, compatible, marked)
|
|
||||||
values, combined, err := space.CompositeOverprint(backdrop.values[:space.Components()], source[:space.Components()], backdrop.alpha, alpha, mode, mask[:space.Components()])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
copy(backdrop.values[:space.Components()], values[:space.Components()])
|
|
||||||
backdrop.alpha = combined
|
|
||||||
backdrop.effect = alpha + backdrop.effect*(1-alpha)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfCompositeOver 按PDF透明模型合成源颜色,四色混合使用分量补色
|
|
||||||
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式
|
|
||||||
// 返回: error 未支持的混合模式
|
|
||||||
func pdfCompositeOver(backdrop *pdfCompositePixel, source [4]float64, alpha float64, space *pdfgo.ColorSpace, mode pdfgo.Name) error {
|
|
||||||
if alpha == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
values, combined, err := space.Composite(backdrop.values[:space.Components()], source[:space.Components()], backdrop.alpha, alpha, mode)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
copy(backdrop.values[:space.Components()], values[:space.Components()])
|
|
||||||
backdrop.alpha = combined
|
|
||||||
backdrop.effect = alpha + backdrop.effect*(1-alpha)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -71,7 +71,7 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha}
|
result[i] = pdfCompositePixel{Values: pixel.Values, Alpha: pixel.Alpha}
|
||||||
}
|
}
|
||||||
candidate := c.acquirePixels(len(backdrop))
|
candidate := c.acquirePixels(len(backdrop))
|
||||||
defer c.releasePixels(candidate)
|
defer c.releasePixels(candidate)
|
||||||
@@ -113,7 +113,7 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
|||||||
if disjoint {
|
if disjoint {
|
||||||
initial = backdrop[index]
|
initial = backdrop[index]
|
||||||
}
|
}
|
||||||
pixels[y*width+x] = pdfCompositePixel{values: initial.values, alpha: initial.alpha}
|
pixels[y*width+x] = pdfCompositePixel{Values: initial.Values, Alpha: initial.Alpha}
|
||||||
if cellChannels != nil {
|
if cellChannels != nil {
|
||||||
cellChannels[y*width+x] = 0
|
cellChannels[y*width+x] = 0
|
||||||
if !disjoint {
|
if !disjoint {
|
||||||
@@ -136,7 +136,7 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
|||||||
for x := range width {
|
for x := range width {
|
||||||
index := (top+y)*c.width + left + x
|
index := (top+y)*c.width + left + x
|
||||||
pdfCompositePatternCell(&result[index], backdrop[index], pixels[y*width+x], disjoint)
|
pdfCompositePatternCell(&result[index], backdrop[index], pixels[y*width+x], disjoint)
|
||||||
if channels != nil && pixels[y*width+x].shape != 0 {
|
if channels != nil && pixels[y*width+x].Shape != 0 {
|
||||||
if disjoint {
|
if disjoint {
|
||||||
channels[index] |= cellChannels[y*width+x]
|
channels[index] |= cellChannels[y*width+x]
|
||||||
} else {
|
} else {
|
||||||
@@ -154,12 +154,12 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pixel := &result[i]
|
pixel := &result[i]
|
||||||
if pixel.effect != 0 {
|
if pixel.Effect != 0 {
|
||||||
for j := 0; j < space.Components(); j++ {
|
for j := 0; j < space.Components(); j++ {
|
||||||
pixel.values[j] = math.Max(0, math.Min(1, (pixel.values[j]*pixel.alpha-backdrop[i].values[j]*backdrop[i].alpha*(1-pixel.effect))/pixel.effect))
|
pixel.Values[j] = math.Max(0, math.Min(1, (pixel.Values[j]*pixel.Alpha-backdrop[i].Values[j]*backdrop[i].Alpha*(1-pixel.Effect))/pixel.Effect))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pixel.alpha = pixel.effect
|
pixel.Alpha = pixel.Effect
|
||||||
}
|
}
|
||||||
complete = true
|
complete = true
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -168,23 +168,23 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
|||||||
// pdfCompositePatternCell 合并互不重叠单元的覆盖贡献,避免相邻抗锯齿边缘重复混合
|
// pdfCompositePatternCell 合并互不重叠单元的覆盖贡献,避免相邻抗锯齿边缘重复混合
|
||||||
// 入参: target 图案累计结果, initial 原始背景, source 单元结果, disjoint 单元边界是否互不重叠
|
// 入参: target 图案累计结果, initial 原始背景, source 单元结果, disjoint 单元边界是否互不重叠
|
||||||
func pdfCompositePatternCell(target *pdfCompositePixel, initial, source pdfCompositePixel, disjoint bool) {
|
func pdfCompositePatternCell(target *pdfCompositePixel, initial, source pdfCompositePixel, disjoint bool) {
|
||||||
if source.shape == 0 {
|
if source.Shape == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if disjoint {
|
if disjoint {
|
||||||
alpha := math.Max(0, math.Min(1, target.alpha+source.alpha-initial.alpha))
|
alpha := math.Max(0, math.Min(1, target.Alpha+source.Alpha-initial.Alpha))
|
||||||
if alpha != 0 {
|
if alpha != 0 {
|
||||||
for i := range target.values {
|
for i := range target.Values {
|
||||||
target.values[i] = (target.values[i]*target.alpha + source.values[i]*source.alpha - initial.values[i]*initial.alpha) / alpha
|
target.Values[i] = (target.Values[i]*target.Alpha + source.Values[i]*source.Alpha - initial.Values[i]*initial.Alpha) / alpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
target.alpha = alpha
|
target.Alpha = alpha
|
||||||
target.effect = math.Min(1, target.effect+source.effect)
|
target.Effect = math.Min(1, target.Effect+source.Effect)
|
||||||
target.shape = math.Min(1, target.shape+source.shape)
|
target.Shape = math.Min(1, target.Shape+source.Shape)
|
||||||
} else {
|
} else {
|
||||||
target.values, target.alpha = source.values, source.alpha
|
target.Values, target.Alpha = source.Values, source.Alpha
|
||||||
target.effect += source.effect * (1 - target.effect)
|
target.Effect += source.Effect * (1 - target.Effect)
|
||||||
target.shape += source.shape * (1 - target.shape)
|
target.Shape += source.Shape * (1 - target.Shape)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,17 +114,17 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo
|
|||||||
}
|
}
|
||||||
remaining := 1.0
|
remaining := 1.0
|
||||||
if shape != 0 {
|
if shape != 0 {
|
||||||
remaining -= math.Min(1, result[i].shape/shape)
|
remaining -= math.Min(1, result[i].Shape/shape)
|
||||||
}
|
}
|
||||||
alpha := result[i].alpha + background[i].alpha*remaining
|
alpha := result[i].Alpha + background[i].Alpha*remaining
|
||||||
if alpha != 0 {
|
if alpha != 0 {
|
||||||
for j := 0; j < space.Components(); j++ {
|
for j := 0; j < space.Components(); j++ {
|
||||||
result[i].values[j] = (result[i].values[j]*result[i].alpha + background[i].values[j]*background[i].alpha*remaining) / alpha
|
result[i].Values[j] = (result[i].Values[j]*result[i].Alpha + background[i].Values[j]*background[i].Alpha*remaining) / alpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result[i].alpha, result[i].effect = alpha, alpha
|
result[i].Alpha, result[i].Effect = alpha, alpha
|
||||||
result[i].shape += background[i].shape * remaining
|
result[i].Shape += background[i].Shape * remaining
|
||||||
if channels != nil && background[i].shape*remaining > 0 {
|
if channels != nil && background[i].Shape*remaining > 0 {
|
||||||
channels[i] |= backgroundChannels[i]
|
channels[i] |= backgroundChannels[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ func (c *pdfCompositor) shadingSourcePixels(node pdfCompositeNode, backdrop []pd
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
for i, pixel := range backdrop {
|
for i, pixel := range backdrop {
|
||||||
result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha}
|
result[i] = pdfCompositePixel{Values: pixel.Values, Alpha: pixel.Alpha}
|
||||||
}
|
}
|
||||||
if err := c.drawMark(node, result, space); err != nil {
|
if err := c.drawMark(node, result, space); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -202,12 +202,12 @@ func (c *pdfCompositor) shadingSourcePixels(node pdfCompositeNode, backdrop []pd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pixel := &result[i]
|
pixel := &result[i]
|
||||||
if pixel.effect != 0 {
|
if pixel.Effect != 0 {
|
||||||
for j := 0; j < space.Components(); j++ {
|
for j := 0; j < space.Components(); j++ {
|
||||||
pixel.values[j] = math.Max(0, math.Min(1, (pixel.values[j]*pixel.alpha-backdrop[i].values[j]*backdrop[i].alpha*(1-pixel.effect))/pixel.effect))
|
pixel.Values[j] = math.Max(0, math.Min(1, (pixel.Values[j]*pixel.Alpha-backdrop[i].Values[j]*backdrop[i].Alpha*(1-pixel.Effect))/pixel.Effect))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pixel.alpha = pixel.effect
|
pixel.Alpha = pixel.Effect
|
||||||
}
|
}
|
||||||
complete = true
|
complete = true
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
+2
-42
@@ -25,38 +25,14 @@ const (
|
|||||||
pdfTransferShape uint8 = 16
|
pdfTransferShape uint8 = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
// pdfTransferKey 区分图形状态、网屏覆盖函数及输出设备
|
// resolveTransfer 无设备函数时保留无缓存快速路径,按需绑定单页解析缓存
|
||||||
type pdfTransferKey struct {
|
|
||||||
transfer *pdfgo.TransferFunction
|
|
||||||
halftone *pdfgo.Halftone
|
|
||||||
model pdfgo.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveTransfer 在页面内复用输出设备的有效函数,不修改原始图形状态
|
|
||||||
// 入参: style 原始图形状态, model 原生设备颜色空间
|
// 入参: style 原始图形状态, model 原生设备颜色空间
|
||||||
// 返回: *pdfgo.TransferFunction 有效函数, error 网屏传递函数错误
|
// 返回: *pdfgo.TransferFunction 有效函数, error 网屏传递函数错误
|
||||||
func (p *pdfImporter) resolveTransfer(style pdfgo.Style, model pdfgo.Name) (*pdfgo.TransferFunction, error) {
|
func (p *pdfImporter) resolveTransfer(style pdfgo.Style, model pdfgo.Name) (*pdfgo.TransferFunction, error) {
|
||||||
if style.Halftone == nil && style.Transfer == nil {
|
if style.Halftone == nil && style.Transfer == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if model == "" {
|
return p.compositingCache().transfers.Resolve(p.reader, style, model)
|
||||||
model = "DeviceRGB"
|
|
||||||
}
|
|
||||||
cache := p.compositingCache()
|
|
||||||
if cache.transfers == nil {
|
|
||||||
cache.transfers = make(map[pdfTransferKey]*pdfgo.TransferFunction)
|
|
||||||
}
|
|
||||||
key := pdfTransferKey{style.Transfer, style.Halftone, model}
|
|
||||||
transfer, found := cache.transfers[key]
|
|
||||||
if !found {
|
|
||||||
var err error
|
|
||||||
transfer, err = p.reader.DeviceTransfer(style.Transfer, style.Halftone, model)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cache.transfers[key] = transfer
|
|
||||||
}
|
|
||||||
return transfer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// compositeHasTransfer 检查最终设备使用的通道,不将备用通道误判为有效函数
|
// compositeHasTransfer 检查最终设备使用的通道,不将备用通道误判为有效函数
|
||||||
@@ -134,22 +110,6 @@ func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pd
|
|||||||
return transfer, nil
|
return transfer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfPaintChannels 选择兼容叠印实际使用源颜色的通道
|
|
||||||
// 入参: values 源四色分量, compatible 是否保留零分量背景, marked 指定通道,nil表示全部
|
|
||||||
// 返回: [4]bool 各通道是否取源颜色
|
|
||||||
func pdfPaintChannels(values [4]float64, compatible bool, marked *[4]bool) [4]bool {
|
|
||||||
channels := [4]bool{true, true, true, true}
|
|
||||||
if marked != nil {
|
|
||||||
channels = *marked
|
|
||||||
}
|
|
||||||
if compatible {
|
|
||||||
for i := range channels {
|
|
||||||
channels[i] = channels[i] && values[i] != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
// opaqueTransfer 复用图案不透明性检查,图案内部函数不提前作用于源颜色
|
// opaqueTransfer 复用图案不透明性检查,图案内部函数不提前作用于源颜色
|
||||||
// 返回: bool 是否允许使用外层设备函数, error 单元解析错误
|
// 返回: bool 是否允许使用外层设备函数, error 单元解析错误
|
||||||
func (p *pdfCompositePattern) opaqueTransfer() (bool, error) {
|
func (p *pdfCompositePattern) opaqueTransfer() (bool, error) {
|
||||||
|
|||||||
+3
-70
@@ -16,16 +16,11 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"math"
|
|
||||||
|
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
"golang.org/x/image/draw"
|
"golang.org/x/image/draw"
|
||||||
"golang.org/x/image/math/f64"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// rasterScaleBufferLimit 限制整像素缩放器的浮点中间缓冲,超出时使用逐行采样
|
|
||||||
const rasterScaleBufferLimit = 8 << 20
|
|
||||||
|
|
||||||
// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
|
// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
|
||||||
// 入参: m 局部到页面变换, height 目标像素高度
|
// 入参: m 局部到页面变换, height 目标像素高度
|
||||||
// 返回: RasterMatrix 局部到像素变换
|
// 返回: RasterMatrix 局部到像素变换
|
||||||
@@ -38,56 +33,13 @@ func (p *RasterPage) PixelTransform(m RasterMatrix, height int) RasterMatrix {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawRasterImage 按页面变换对图像执行高质量采样
|
|
||||||
// 入参: dst 目标图像, src 源图像, m 像素坐标变换
|
|
||||||
func drawRasterImage(dst draw.Image, src image.Image, m RasterMatrix) {
|
|
||||||
drawRasterImagePixels(dst, src, m, draw.Over)
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawRasterImagePixels 对轴向缩小使用可分离采样,其余变换保留通用路径
|
|
||||||
// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作
|
|
||||||
func drawRasterImagePixels(dst draw.Image, src image.Image, m RasterMatrix, 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 RasterMatrix) (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
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawRasterImageCommand 使用当前后端生成裁剪蒙版,共用高质量图像采样
|
// drawRasterImageCommand 使用当前后端生成裁剪蒙版,共用高质量图像采样
|
||||||
// 入参: backend 当前后端, page 页面, dst 目标图像, src 原始图像, command 图像指令, cache 蒙版缓存
|
// 入参: backend 当前后端, page 页面, dst 目标图像, src 原始图像, command 图像指令, cache 蒙版缓存
|
||||||
// 返回: error 裁剪绘制错误
|
// 返回: error 裁剪绘制错误
|
||||||
func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Image, src image.Image, command RasterCommand, cache *renderCache[[32]byte, *image.Alpha]) error {
|
func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Image, src image.Image, command RasterCommand, cache *renderCache[[32]byte, *image.Alpha]) error {
|
||||||
m := page.PixelTransform(command.Transform, dst.Bounds().Dy())
|
m := page.PixelTransform(command.Transform, dst.Bounds().Dy())
|
||||||
if command.Clip == nil {
|
if command.Clip == nil {
|
||||||
drawRasterImage(dst, src, m)
|
baseimage.DrawPixels(dst, src, m, draw.Over)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
mask, err := rasterClipMask(backend, page, command.Clip, cache)
|
mask, err := rasterClipMask(backend, page, command.Clip, cache)
|
||||||
@@ -97,31 +49,12 @@ func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Im
|
|||||||
if mask.Rect.Empty() {
|
if mask.Rect.Empty() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
bounds := mask.Rect.Intersect(dst.Bounds()).Intersect(rasterImageBounds(src.Bounds(), m, dst.Bounds()))
|
bounds := mask.Rect.Intersect(dst.Bounds()).Intersect(baseimage.TransformBounds(src.Bounds(), m, dst.Bounds()))
|
||||||
if bounds.Empty() {
|
if bounds.Empty() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
layer := image.NewRGBA64(bounds)
|
layer := image.NewRGBA64(bounds)
|
||||||
drawRasterImagePixels(layer, baseimage.PixelSource(src), m, draw.Src)
|
baseimage.DrawPixels(layer, baseimage.PixelSource(src), m, draw.Src)
|
||||||
draw.DrawMask(dst, bounds, layer, bounds.Min, mask, bounds.Min, draw.Over)
|
draw.DrawMask(dst, bounds, layer, bounds.Min, mask, bounds.Min, draw.Over)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// rasterImageBounds 计算采样器可能写入的像素范围,并在整数转换前裁至目标边界
|
|
||||||
// 入参: source 原始像素边界, m 像素变换, target 目标边界
|
|
||||||
// 返回: image.Rectangle 受影响的目标像素区域
|
|
||||||
func rasterImageBounds(source image.Rectangle, m RasterMatrix, 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))
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -133,7 +134,7 @@ func (r *Renderer) positionText(object TextObject, state RenderState, contentOnl
|
|||||||
bold := weight >= 700
|
bold := weight >= 700
|
||||||
if definition := r.Reader.fontDefinition(id); definition != nil {
|
if definition := r.Reader.fontDefinition(id); definition != nil {
|
||||||
bold = bold && !definition.Bold
|
bold = bold && !definition.Bold
|
||||||
if definition.FontFile == "" && fontNoSyntheticBold(definition.FontName, definition.FamilyName) {
|
if definition.FontFile == "" && basefont.NoSyntheticBold(definition.FontName, definition.FamilyName) {
|
||||||
bold = false
|
bold = false
|
||||||
}
|
}
|
||||||
italic = italic && !definition.Italic
|
italic = italic && !definition.Italic
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
|
||||||
)
|
|
||||||
|
|
||||||
// canvasGeometryDigest 按完整坐标、路径顺序及几何容差计算摘要
|
|
||||||
// 入参: paths 非空路径序列
|
|
||||||
// 返回: [32]byte 几何摘要
|
|
||||||
func canvasGeometryDigest(paths ...*canvas.Path) [32]byte {
|
|
||||||
hash := sha256.New()
|
|
||||||
var data [512]byte
|
|
||||||
binary.LittleEndian.PutUint64(data[:8], math.Float64bits(canvas.Tolerance))
|
|
||||||
binary.LittleEndian.PutUint64(data[8:16], math.Float64bits(canvas.Epsilon))
|
|
||||||
binary.LittleEndian.PutUint64(data[16:24], math.Float64bits(canvas.BentleyOttmannEpsilon))
|
|
||||||
hash.Write(data[:24])
|
|
||||||
for _, path := range paths {
|
|
||||||
values := path.Data()
|
|
||||||
binary.LittleEndian.PutUint64(data[:8], uint64(len(values)))
|
|
||||||
hash.Write(data[:8])
|
|
||||||
for len(values) > 0 {
|
|
||||||
count := min(len(values), len(data)/8)
|
|
||||||
for i, value := range values[:count] {
|
|
||||||
binary.LittleEndian.PutUint64(data[i*8:], math.Float64bits(value))
|
|
||||||
}
|
|
||||||
hash.Write(data[:count*8])
|
|
||||||
values = values[count:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var key [32]byte
|
|
||||||
hash.Sum(key[:0])
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// settleCanvasPath 复用复杂路径的填充规范化结果,返回独立副本
|
|
||||||
// 入参: path 页面坐标路径, rule 填充规则
|
|
||||||
// 返回: *canvas.Path 规范化填充轮廓
|
|
||||||
func (r *Renderer) settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path {
|
|
||||||
if len(path.Data()) < 256 {
|
|
||||||
return path.Settle(rule)
|
|
||||||
}
|
|
||||||
key := canvasFillKey{digest: canvasGeometryDigest(path), rule: rule}
|
|
||||||
cache := r.canvasSettledPaths()
|
|
||||||
if result, ok := cache.get(key); ok {
|
|
||||||
return result.Copy()
|
|
||||||
}
|
|
||||||
result := path.Settle(rule)
|
|
||||||
if cost := len(result.Data())*8 + 256; cost <= cache.limit {
|
|
||||||
cache.put(key, result.Copy(), cost)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// clipRenderer 收集图形和文字的绘制轮廓
|
// clipRenderer 收集图形和文字的绘制轮廓
|
||||||
@@ -262,6 +263,117 @@ func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
|
|||||||
return parent.And(current)
|
return parent.And(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// intersectConvexCanvasPaths 逐边裁剪单个凸多边形,避免仿射矩形共边进入曲线扫线
|
||||||
|
// 入参: left 左侧路径, right 右侧路径
|
||||||
|
// 返回: *canvas.Path 交集路径, bool 是否适用凸多边形裁剪
|
||||||
|
func intersectConvexCanvasPaths(left, right *canvas.Path) (*canvas.Path, bool) {
|
||||||
|
a, ok := convexCanvasPolygon(left)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
b, ok := convexCanvasPolygon(right)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return intersectCanvasPolygons(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// intersectLinearCanvasContours 精确裁剪直线轮廓,不展开曲线或改变填充绕数
|
||||||
|
// 入参: path 绘制路径, clip 单个凸裁剪窗口
|
||||||
|
// 返回: *canvas.Path 交集路径, bool 是否适用直线轮廓裁剪
|
||||||
|
func intersectLinearCanvasContours(path, clip *canvas.Path) (*canvas.Path, bool) {
|
||||||
|
if path == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
data := path.Data()
|
||||||
|
for i := 0; i < len(data); i += 4 {
|
||||||
|
if i+4 > len(data) || data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return intersectCanvasContours(path, clip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// intersectCanvasContours 按凸窗口裁剪闭合轮廓,保留绕向、孔洞和重叠透明度
|
||||||
|
// 入参: path 绘制路径, clip 单个凸裁剪路径
|
||||||
|
// 返回: *canvas.Path 交集路径, bool 是否适用分段裁剪
|
||||||
|
func intersectCanvasContours(path, clip *canvas.Path) (*canvas.Path, bool) {
|
||||||
|
if path == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
b, ok := convexCanvasPolygon(clip)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
parts := path.Split()
|
||||||
|
polygons := make([][]canvas.Point, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
part = part.Flatten(canvas.Tolerance)
|
||||||
|
if !part.Closed() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
a := part.Coords()
|
||||||
|
if len(a) < 4 || a[0] != a[len(a)-1] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, point := range a {
|
||||||
|
if !finite(point.X) || !finite(point.Y) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
polygons = append(polygons, a[:len(a)-1])
|
||||||
|
}
|
||||||
|
result := &canvas.Path{}
|
||||||
|
for _, polygon := range polygons {
|
||||||
|
part, ok := intersectCanvasPolygons(polygon, b)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
result = result.Append(part)
|
||||||
|
}
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// intersectCanvasPolygons 按凸窗口逐边裁剪多边形,保留原顶点绕向
|
||||||
|
// 入参: a 绘制顶点, b 裁剪顶点
|
||||||
|
// 返回: *canvas.Path 交集路径, bool 是否满足数值精度
|
||||||
|
func intersectCanvasPolygons(a, b []canvas.Point) (*canvas.Path, bool) {
|
||||||
|
a, ok := basegeom.ClipPolygon(a, b)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
result := &canvas.Path{}
|
||||||
|
if len(a) >= 3 {
|
||||||
|
result.MoveTo(a[0].X, a[0].Y)
|
||||||
|
for _, p := range a[1:] {
|
||||||
|
result.LineTo(p.X, p.Y)
|
||||||
|
}
|
||||||
|
result.Close()
|
||||||
|
}
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// convexCanvasPolygon 检查单个闭合直线轮廓的所有顶点位于各边的同一侧
|
||||||
|
// 入参: path 路径
|
||||||
|
// 返回: []canvas.Point 独立顶点, bool 是否为非退化凸多边形
|
||||||
|
func convexCanvasPolygon(path *canvas.Path) ([]canvas.Point, bool) {
|
||||||
|
if path == nil || path.HasSubpaths() || !path.Closed() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
data := path.Data()
|
||||||
|
for i := 0; i < len(data); i += 4 {
|
||||||
|
if i+4 > len(data) || data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
points := path.Coords()
|
||||||
|
if len(points) < 4 || points[0] != points[len(points)-1] {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
points = points[:len(points)-1]
|
||||||
|
return basegeom.ConvexPolygon(points)
|
||||||
|
}
|
||||||
|
|
||||||
// unionClipPath 合并裁剪区域
|
// unionClipPath 合并裁剪区域
|
||||||
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
|
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
|
||||||
// 返回: *canvas.Path 合并后的裁剪路径
|
// 返回: *canvas.Path 合并后的裁剪路径
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
|
||||||
)
|
|
||||||
|
|
||||||
// intersectConvexCanvasPaths 逐边裁剪单个凸多边形,避免仿射矩形共边进入曲线扫线
|
|
||||||
// 入参: left 左侧路径, right 右侧路径
|
|
||||||
// 返回: *canvas.Path 交集路径, bool 是否适用凸多边形裁剪
|
|
||||||
func intersectConvexCanvasPaths(left, right *canvas.Path) (*canvas.Path, bool) {
|
|
||||||
a, ok := convexCanvasPolygon(left)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
b, ok := convexCanvasPolygon(right)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return intersectCanvasPolygons(a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// intersectLinearCanvasContours 精确裁剪直线轮廓,不展开曲线或改变填充绕数
|
|
||||||
// 入参: path 绘制路径, clip 单个凸裁剪窗口
|
|
||||||
// 返回: *canvas.Path 交集路径, bool 是否适用直线轮廓裁剪
|
|
||||||
func intersectLinearCanvasContours(path, clip *canvas.Path) (*canvas.Path, bool) {
|
|
||||||
if path == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
data := path.Data()
|
|
||||||
for i := 0; i < len(data); i += 4 {
|
|
||||||
if i+4 > len(data) || data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return intersectCanvasContours(path, clip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// intersectCanvasContours 按凸窗口裁剪闭合轮廓,保留绕向、孔洞和重叠透明度
|
|
||||||
// 入参: path 绘制路径, clip 单个凸裁剪路径
|
|
||||||
// 返回: *canvas.Path 交集路径, bool 是否适用分段裁剪
|
|
||||||
func intersectCanvasContours(path, clip *canvas.Path) (*canvas.Path, bool) {
|
|
||||||
if path == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
b, ok := convexCanvasPolygon(clip)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
parts := path.Split()
|
|
||||||
polygons := make([][]canvas.Point, 0, len(parts))
|
|
||||||
for _, part := range parts {
|
|
||||||
part = part.Flatten(canvas.Tolerance)
|
|
||||||
if !part.Closed() {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
a := part.Coords()
|
|
||||||
if len(a) < 4 || a[0] != a[len(a)-1] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, point := range a {
|
|
||||||
if !finite(point.X) || !finite(point.Y) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
polygons = append(polygons, a[:len(a)-1])
|
|
||||||
}
|
|
||||||
result := &canvas.Path{}
|
|
||||||
for _, polygon := range polygons {
|
|
||||||
part, ok := intersectCanvasPolygons(polygon, b)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
result = result.Append(part)
|
|
||||||
}
|
|
||||||
return result, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// intersectCanvasPolygons 按凸窗口逐边裁剪多边形,保留原顶点绕向
|
|
||||||
// 入参: a 绘制顶点, b 裁剪顶点
|
|
||||||
// 返回: *canvas.Path 交集路径, bool 是否满足数值精度
|
|
||||||
func intersectCanvasPolygons(a, b []canvas.Point) (*canvas.Path, bool) {
|
|
||||||
for i, start := range b {
|
|
||||||
end := b[(i+1)%len(b)]
|
|
||||||
dx, dy := end.X-start.X, end.Y-start.Y
|
|
||||||
length := math.Hypot(dx, dy)
|
|
||||||
if length == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dx, dy = dx/length, dy/length
|
|
||||||
distance := func(p canvas.Point) float64 { return convexCanvasDistance(start, 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 []canvas.Point
|
|
||||||
appendPoint := func(p canvas.Point) {
|
|
||||||
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 := basegeom.Lerp(Point{X: previous.X, Y: previous.Y}, Point{X: p.X, Y: p.Y}, fraction)
|
|
||||||
if dx == 0 {
|
|
||||||
intersection.X = start.X
|
|
||||||
}
|
|
||||||
if dy == 0 {
|
|
||||||
intersection.Y = start.Y
|
|
||||||
}
|
|
||||||
appendPoint(canvas.Point{X: intersection.X, Y: intersection.Y})
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
result := &canvas.Path{}
|
|
||||||
if len(a) >= 3 {
|
|
||||||
result.MoveTo(a[0].X, a[0].Y)
|
|
||||||
for _, p := range a[1:] {
|
|
||||||
result.LineTo(p.X, p.Y)
|
|
||||||
}
|
|
||||||
result.Close()
|
|
||||||
}
|
|
||||||
return result, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// convexCanvasPolygon 检查单个闭合直线轮廓的所有顶点位于各边的同一侧
|
|
||||||
// 入参: path 路径
|
|
||||||
// 返回: []canvas.Point 独立顶点, bool 是否为非退化凸多边形
|
|
||||||
func convexCanvasPolygon(path *canvas.Path) ([]canvas.Point, bool) {
|
|
||||||
if path == nil || path.HasSubpaths() || !path.Closed() {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
data := path.Data()
|
|
||||||
for i := 0; i < len(data); i += 4 {
|
|
||||||
if i+4 > len(data) || data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
points := path.Coords()
|
|
||||||
if len(points) < 4 || points[0] != points[len(points)-1] {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
points = points[:len(points)-1]
|
|
||||||
positive := false
|
|
||||||
for i, start := range points {
|
|
||||||
end := points[(i+1)%len(points)]
|
|
||||||
dx, dy := end.X-start.X, end.Y-start.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 := convexCanvasDistance(start, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// convexCanvasDistance 按浮点乘法误差界识别共线点,不使用固定坐标容差
|
|
||||||
// 入参: start 边起点, point 待测顶点, dx 横向单位分量, dy 纵向单位分量
|
|
||||||
// 返回: float64 顶点到边的有向距离
|
|
||||||
func convexCanvasDistance(start, point canvas.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
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,10 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
"image"
|
"image"
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
||||||
@@ -152,3 +155,50 @@ func (s *canvasBackendState) resetFonts() {
|
|||||||
s.textGlyphPathCache = renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue]{limit: 16 << 20}
|
s.textGlyphPathCache = renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue]{limit: 16 << 20}
|
||||||
s.fontOutlines = make(map[*canvas.Font]*basefont.Outliner)
|
s.fontOutlines = make(map[*canvas.Font]*basefont.Outliner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// canvasGeometryDigest 按完整坐标、路径顺序及几何容差计算摘要
|
||||||
|
// 入参: paths 非空路径序列
|
||||||
|
// 返回: [32]byte 几何摘要
|
||||||
|
func canvasGeometryDigest(paths ...*canvas.Path) [32]byte {
|
||||||
|
hash := sha256.New()
|
||||||
|
var data [512]byte
|
||||||
|
binary.LittleEndian.PutUint64(data[:8], math.Float64bits(canvas.Tolerance))
|
||||||
|
binary.LittleEndian.PutUint64(data[8:16], math.Float64bits(canvas.Epsilon))
|
||||||
|
binary.LittleEndian.PutUint64(data[16:24], math.Float64bits(canvas.BentleyOttmannEpsilon))
|
||||||
|
hash.Write(data[:24])
|
||||||
|
for _, path := range paths {
|
||||||
|
values := path.Data()
|
||||||
|
binary.LittleEndian.PutUint64(data[:8], uint64(len(values)))
|
||||||
|
hash.Write(data[:8])
|
||||||
|
for len(values) > 0 {
|
||||||
|
count := min(len(values), len(data)/8)
|
||||||
|
for i, value := range values[:count] {
|
||||||
|
binary.LittleEndian.PutUint64(data[i*8:], math.Float64bits(value))
|
||||||
|
}
|
||||||
|
hash.Write(data[:count*8])
|
||||||
|
values = values[count:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var key [32]byte
|
||||||
|
hash.Sum(key[:0])
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// settleCanvasPath 复用复杂路径的填充规范化结果,返回独立副本
|
||||||
|
// 入参: path 页面坐标路径, rule 填充规则
|
||||||
|
// 返回: *canvas.Path 规范化填充轮廓
|
||||||
|
func (r *Renderer) settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path {
|
||||||
|
if len(path.Data()) < 256 {
|
||||||
|
return path.Settle(rule)
|
||||||
|
}
|
||||||
|
key := canvasFillKey{digest: canvasGeometryDigest(path), rule: rule}
|
||||||
|
cache := r.canvasSettledPaths()
|
||||||
|
if result, ok := cache.get(key); ok {
|
||||||
|
return result.Copy()
|
||||||
|
}
|
||||||
|
result := path.Settle(rule)
|
||||||
|
if cost := len(result.Data())*8 + 256; cost <= cache.limit {
|
||||||
|
cache.put(key, result.Copy(), cost)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
|
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
embeddedFont := false
|
embeddedFont := false
|
||||||
if of := r.Reader.fontDefinition(fontID); of != nil {
|
if of := r.Reader.fontDefinition(fontID); of != nil {
|
||||||
embeddedFont = of.FontFile != ""
|
embeddedFont = of.FontFile != ""
|
||||||
if syntheticBold && !embeddedFont && fontNoSyntheticBold(of.FontName, of.FamilyName) {
|
if syntheticBold && !embeddedFont && basefont.NoSyntheticBold(of.FontName, of.FamilyName) {
|
||||||
syntheticBold = false
|
syntheticBold = false
|
||||||
}
|
}
|
||||||
if of.Bold {
|
if of.Bold {
|
||||||
|
|||||||
+103
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -340,3 +341,105 @@ func textCodeRunesContext(ctx context.Context, value string) ([]rune, error) {
|
|||||||
}
|
}
|
||||||
return runes[:written], nil
|
return runes[:written], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML 解析字形变换,省略的字符数和字形数均取1
|
||||||
|
// 入参: decoder XML解码器, start 变换起始节点
|
||||||
|
// 返回: error 解码错误
|
||||||
|
func (transform *CGTransform) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
|
||||||
|
*transform = CGTransform{CodeCount: 1, GlyphCount: 1}
|
||||||
|
for _, attr := range start.Attr {
|
||||||
|
if attr.Name.Space != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var target *int
|
||||||
|
switch attr.Name.Local {
|
||||||
|
case "CodePosition":
|
||||||
|
target = &transform.CodePosition
|
||||||
|
case "CodeCount":
|
||||||
|
target = &transform.CodeCount
|
||||||
|
case "GlyphCount":
|
||||||
|
target = &transform.GlyphCount
|
||||||
|
}
|
||||||
|
if target != nil {
|
||||||
|
value, err := strconv.Atoi(strings.TrimSpace(attr.Value))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*target = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error {
|
||||||
|
if node.Name.Local == "Glyphs" {
|
||||||
|
return d.DecodeElement(&transform.Glyphs, &node)
|
||||||
|
}
|
||||||
|
return d.Skip()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML 解析文字定位及原文,不使用带外部命名空间的定位属性
|
||||||
|
// 入参: decoder XML解码器, start 文字节点
|
||||||
|
// 返回: error 解码错误
|
||||||
|
func (code *TextCode) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
|
||||||
|
type plain TextCode
|
||||||
|
var value plain
|
||||||
|
start = ofdXMLStart(start)
|
||||||
|
if err := decoder.DecodeElement(&value, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*code = TextCode(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML 解析标准文字节点,将各TextCode前的局部字形位置统一为对象内索引
|
||||||
|
// 入参: decoder XML解码器, start 对象起始节点
|
||||||
|
// 返回: error 解码错误
|
||||||
|
func (obj *TextObject) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
|
||||||
|
type plain TextObject
|
||||||
|
var value plain
|
||||||
|
if err := decodeGraphicAttributes(start, &value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
offset, pending := 0, 0
|
||||||
|
if err := decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error {
|
||||||
|
switch node.Name.Local {
|
||||||
|
case "FillColor":
|
||||||
|
return d.DecodeElement(&value.FillColor, &node)
|
||||||
|
case "StrokeColor":
|
||||||
|
return d.DecodeElement(&value.StrokeColor, &node)
|
||||||
|
case "Clips":
|
||||||
|
return d.DecodeElement(&value.Clips, &node)
|
||||||
|
case "Actions":
|
||||||
|
return decodeOFDActions(d, node, &value.Actions)
|
||||||
|
case "CGTransform":
|
||||||
|
return d.DecodeElement(&value.CGTransform, &node)
|
||||||
|
case "TextCode":
|
||||||
|
var code TextCode
|
||||||
|
if err := d.DecodeElement(&code, &node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := pending; i < len(value.CGTransform); i++ {
|
||||||
|
value.CGTransform[i].CodePosition += offset
|
||||||
|
}
|
||||||
|
pending = len(value.CGTransform)
|
||||||
|
offset += len(textCodeRunes(code.Value))
|
||||||
|
value.TextCode = append(value.TextCode, code)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.Skip()
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*obj = TextObject(value)
|
||||||
|
for _, attr := range ofdXMLStart(start).Attr {
|
||||||
|
if attr.Name.Local == "LineWidth" {
|
||||||
|
obj.LineWidthSet = obj.LineWidth == 0
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "MiterLimit" {
|
||||||
|
obj.MiterLimitSet = obj.MiterLimit == 0
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "DashPattern" {
|
||||||
|
obj.dashPatternSet = obj.DashPattern == ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"encoding/xml"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UnmarshalXML 解析字形变换,省略的字符数和字形数均取1
|
|
||||||
// 入参: decoder XML解码器, start 变换起始节点
|
|
||||||
// 返回: error 解码错误
|
|
||||||
func (transform *CGTransform) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
|
|
||||||
*transform = CGTransform{CodeCount: 1, GlyphCount: 1}
|
|
||||||
for _, attr := range start.Attr {
|
|
||||||
if attr.Name.Space != "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var target *int
|
|
||||||
switch attr.Name.Local {
|
|
||||||
case "CodePosition":
|
|
||||||
target = &transform.CodePosition
|
|
||||||
case "CodeCount":
|
|
||||||
target = &transform.CodeCount
|
|
||||||
case "GlyphCount":
|
|
||||||
target = &transform.GlyphCount
|
|
||||||
}
|
|
||||||
if target != nil {
|
|
||||||
value, err := strconv.Atoi(strings.TrimSpace(attr.Value))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*target = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error {
|
|
||||||
if node.Name.Local == "Glyphs" {
|
|
||||||
return d.DecodeElement(&transform.Glyphs, &node)
|
|
||||||
}
|
|
||||||
return d.Skip()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML 解析文字定位及原文,不使用带外部命名空间的定位属性
|
|
||||||
// 入参: decoder XML解码器, start 文字节点
|
|
||||||
// 返回: error 解码错误
|
|
||||||
func (code *TextCode) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
|
|
||||||
type plain TextCode
|
|
||||||
var value plain
|
|
||||||
start = ofdXMLStart(start)
|
|
||||||
if err := decoder.DecodeElement(&value, &start); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*code = TextCode(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML 解析标准文字节点,将各TextCode前的局部字形位置统一为对象内索引
|
|
||||||
// 入参: decoder XML解码器, start 对象起始节点
|
|
||||||
// 返回: error 解码错误
|
|
||||||
func (obj *TextObject) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
|
|
||||||
type plain TextObject
|
|
||||||
var value plain
|
|
||||||
if err := decodeGraphicAttributes(start, &value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
offset, pending := 0, 0
|
|
||||||
if err := decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error {
|
|
||||||
switch node.Name.Local {
|
|
||||||
case "FillColor":
|
|
||||||
return d.DecodeElement(&value.FillColor, &node)
|
|
||||||
case "StrokeColor":
|
|
||||||
return d.DecodeElement(&value.StrokeColor, &node)
|
|
||||||
case "Clips":
|
|
||||||
return d.DecodeElement(&value.Clips, &node)
|
|
||||||
case "Actions":
|
|
||||||
return decodeOFDActions(d, node, &value.Actions)
|
|
||||||
case "CGTransform":
|
|
||||||
return d.DecodeElement(&value.CGTransform, &node)
|
|
||||||
case "TextCode":
|
|
||||||
var code TextCode
|
|
||||||
if err := d.DecodeElement(&code, &node); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i := pending; i < len(value.CGTransform); i++ {
|
|
||||||
value.CGTransform[i].CodePosition += offset
|
|
||||||
}
|
|
||||||
pending = len(value.CGTransform)
|
|
||||||
offset += len(textCodeRunes(code.Value))
|
|
||||||
value.TextCode = append(value.TextCode, code)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.Skip()
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*obj = TextObject(value)
|
|
||||||
for _, attr := range ofdXMLStart(start).Attr {
|
|
||||||
if attr.Name.Local == "LineWidth" {
|
|
||||||
obj.LineWidthSet = obj.LineWidth == 0
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "MiterLimit" {
|
|
||||||
obj.MiterLimitSet = obj.MiterLimit == 0
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "DashPattern" {
|
|
||||||
obj.dashPatternSet = obj.DashPattern == ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"archive/zip"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExtractVersion 将当前文档的指定版本提取为独立阅读器,可继续编辑或另存,不改变原包
|
|
||||||
// 仅保留版本文件清单中的文件并重建单文档入口,不保留版本列表;原签名可能因入口变化而失效
|
|
||||||
// 文件内容按需读取,使用期间不得关闭持有输入文件的原阅读器;返回值不关闭输入文件
|
|
||||||
// 入参: id 版本标识,空值选择唯一的Current版本,没有默认版本或存在歧义时返回错误
|
|
||||||
// 返回: *Reader 版本阅读器, error 选择、读取或文件缺失错误
|
|
||||||
func (r *Reader) ExtractVersion(id string) (*Reader, error) {
|
|
||||||
return r.ExtractVersionContext(context.Background(), id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractVersionContext 将指定版本提取为独立阅读器,扫描、读取和解析时响应取消
|
|
||||||
// 文件保留范围、入口选择和输入文件生命周期与ExtractVersion相同,取消或失败不改变原包
|
|
||||||
// 入参: ctx 取消上下文, id 版本标识,空值选择唯一的Current版本
|
|
||||||
// 返回: *Reader 版本阅读器, error 选择、读取或取消错误
|
|
||||||
func (r *Reader) ExtractVersionContext(ctx context.Context, id string) (*Reader, error) {
|
|
||||||
entries, err := r.documentVersionEntriesContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var selected *DocumentVersionInfo
|
|
||||||
for i := range entries {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
entry := &entries[i]
|
|
||||||
if id != "" && entry.ID == id || id == "" && entry.Current {
|
|
||||||
if selected != nil {
|
|
||||||
return nil, fmt.Errorf("multiple current document versions")
|
|
||||||
}
|
|
||||||
selected = entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if selected == nil {
|
|
||||||
if id == "" {
|
|
||||||
return nil, fmt.Errorf("no current document version")
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("document version not found: %s", id)
|
|
||||||
}
|
|
||||||
version, err := r.readDocumentVersionContext(ctx, selected.ID, selected.Location)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("version %s: %w", selected.ID, err)
|
|
||||||
}
|
|
||||||
files := make(map[string]bool, len(version.Files))
|
|
||||||
for _, file := range version.Files {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !r.fileNames[file.Location] {
|
|
||||||
return nil, fmt.Errorf("version %s file not found: %s", selected.ID, file.Location)
|
|
||||||
}
|
|
||||||
files[file.Location] = true
|
|
||||||
}
|
|
||||||
root, err := r.extractedVersionRoot(ctx, version, files)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
next := &Reader{Path: r.Path, files: map[string][]byte{"OFD.xml": root}, encryption: r.encryption}
|
|
||||||
for name := range files {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if name != "OFD.xml" {
|
|
||||||
if data, ok := r.files[name]; ok {
|
|
||||||
next.files[name] = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if r.Zip != nil {
|
|
||||||
next.Zip = &zip.Reader{Comment: r.Zip.Comment}
|
|
||||||
for _, file := range r.Zip.File {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
name := cleanPackagePath(file.Name)
|
|
||||||
if name != "OFD.xml" && files[name] && !file.FileInfo().IsDir() {
|
|
||||||
next.Zip.File = append(next.Zip.File, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := next.initRootContext(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := next.docStructureContext(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return next, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractedVersionRoot 重建版本副本入口,保留元数据及命名空间,移除其他文档与版本列表
|
|
||||||
// 入参: ctx 取消上下文, version 版本描述, files 版本包含的文件
|
|
||||||
// 返回: []byte 主入口XML, error 读取、结构或取消错误
|
|
||||||
func (r *Reader) extractedVersionRoot(ctx context.Context, version DocumentVersionInfo, files map[string]bool) ([]byte, error) {
|
|
||||||
data, err := r.readFileViewContext(ctx, "OFD.xml")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
root, err := parseEditorXMLContext(ctx, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
body := root.childAt("DocBody", r.documentIndex)
|
|
||||||
if body == nil {
|
|
||||||
return nil, fmt.Errorf("document entry not found: %d", r.documentIndex)
|
|
||||||
}
|
|
||||||
var patches []editorXMLPatch
|
|
||||||
for _, child := range root.children {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if child.matchesOFD("DocBody") && child != body {
|
|
||||||
patches = append(patches, editorXMLPatch{start: child.start, end: child.end})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entry := body.child("DocRoot")
|
|
||||||
if entry != nil {
|
|
||||||
patches = append(patches, editorXMLContent(data, entry, editorFontXMLText(version.DocRoot)))
|
|
||||||
}
|
|
||||||
if versions := body.child("Versions"); versions != nil {
|
|
||||||
var replacement []byte
|
|
||||||
if entry == nil {
|
|
||||||
replacement = body.textXML("DocRoot", version.DocRoot)
|
|
||||||
}
|
|
||||||
patches = append(patches, editorXMLPatch{versions.start, versions.end, replacement})
|
|
||||||
} else if entry == nil {
|
|
||||||
return nil, fmt.Errorf("document entry not found: %d", r.documentIndex)
|
|
||||||
}
|
|
||||||
references := []*editorXML{body.child("Signatures")}
|
|
||||||
if info := body.child("DocInfo"); info != nil {
|
|
||||||
references = append(references, info.child("Cover"))
|
|
||||||
}
|
|
||||||
for _, node := range references {
|
|
||||||
if node == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := resolveResourcePath("OFD.xml", "", editorImportText(data, node))
|
|
||||||
if files[name] {
|
|
||||||
patches = append(patches, editorXMLContent(data, node, editorFontXMLText("/"+name)))
|
|
||||||
} else {
|
|
||||||
patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = editorPatchXML(data, patches)
|
|
||||||
if version.Version != "" {
|
|
||||||
root, err = parseEditorXMLContext(ctx, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data, err = editorXMLAttribute(data, root, "Version", version.Version)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
err = ctx.Err()
|
|
||||||
}
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
@@ -153,3 +154,157 @@ func (r *Reader) versionSignatures(location string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractVersion 将当前文档的指定版本提取为独立阅读器,可继续编辑或另存,不改变原包
|
||||||
|
// 仅保留版本文件清单中的文件并重建单文档入口,不保留版本列表;原签名可能因入口变化而失效
|
||||||
|
// 文件内容按需读取,使用期间不得关闭持有输入文件的原阅读器;返回值不关闭输入文件
|
||||||
|
// 入参: id 版本标识,空值选择唯一的Current版本,没有默认版本或存在歧义时返回错误
|
||||||
|
// 返回: *Reader 版本阅读器, error 选择、读取或文件缺失错误
|
||||||
|
func (r *Reader) ExtractVersion(id string) (*Reader, error) {
|
||||||
|
return r.ExtractVersionContext(context.Background(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractVersionContext 将指定版本提取为独立阅读器,扫描、读取和解析时响应取消
|
||||||
|
// 文件保留范围、入口选择和输入文件生命周期与ExtractVersion相同,取消或失败不改变原包
|
||||||
|
// 入参: ctx 取消上下文, id 版本标识,空值选择唯一的Current版本
|
||||||
|
// 返回: *Reader 版本阅读器, error 选择、读取或取消错误
|
||||||
|
func (r *Reader) ExtractVersionContext(ctx context.Context, id string) (*Reader, error) {
|
||||||
|
entries, err := r.documentVersionEntriesContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var selected *DocumentVersionInfo
|
||||||
|
for i := range entries {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entry := &entries[i]
|
||||||
|
if id != "" && entry.ID == id || id == "" && entry.Current {
|
||||||
|
if selected != nil {
|
||||||
|
return nil, fmt.Errorf("multiple current document versions")
|
||||||
|
}
|
||||||
|
selected = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
if id == "" {
|
||||||
|
return nil, fmt.Errorf("no current document version")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("document version not found: %s", id)
|
||||||
|
}
|
||||||
|
version, err := r.readDocumentVersionContext(ctx, selected.ID, selected.Location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("version %s: %w", selected.ID, err)
|
||||||
|
}
|
||||||
|
files := make(map[string]bool, len(version.Files))
|
||||||
|
for _, file := range version.Files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !r.fileNames[file.Location] {
|
||||||
|
return nil, fmt.Errorf("version %s file not found: %s", selected.ID, file.Location)
|
||||||
|
}
|
||||||
|
files[file.Location] = true
|
||||||
|
}
|
||||||
|
root, err := r.extractedVersionRoot(ctx, version, files)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
next := &Reader{Path: r.Path, files: map[string][]byte{"OFD.xml": root}, encryption: r.encryption}
|
||||||
|
for name := range files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if name != "OFD.xml" {
|
||||||
|
if data, ok := r.files[name]; ok {
|
||||||
|
next.files[name] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.Zip != nil {
|
||||||
|
next.Zip = &zip.Reader{Comment: r.Zip.Comment}
|
||||||
|
for _, file := range r.Zip.File {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name := cleanPackagePath(file.Name)
|
||||||
|
if name != "OFD.xml" && files[name] && !file.FileInfo().IsDir() {
|
||||||
|
next.Zip.File = append(next.Zip.File, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := next.initRootContext(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := next.docStructureContext(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractedVersionRoot 重建版本副本入口,保留元数据及命名空间,移除其他文档与版本列表
|
||||||
|
// 入参: ctx 取消上下文, version 版本描述, files 版本包含的文件
|
||||||
|
// 返回: []byte 主入口XML, error 读取、结构或取消错误
|
||||||
|
func (r *Reader) extractedVersionRoot(ctx context.Context, version DocumentVersionInfo, files map[string]bool) ([]byte, error) {
|
||||||
|
data, err := r.readFileViewContext(ctx, "OFD.xml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body := root.childAt("DocBody", r.documentIndex)
|
||||||
|
if body == nil {
|
||||||
|
return nil, fmt.Errorf("document entry not found: %d", r.documentIndex)
|
||||||
|
}
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
for _, child := range root.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if child.matchesOFD("DocBody") && child != body {
|
||||||
|
patches = append(patches, editorXMLPatch{start: child.start, end: child.end})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry := body.child("DocRoot")
|
||||||
|
if entry != nil {
|
||||||
|
patches = append(patches, editorXMLContent(data, entry, editorFontXMLText(version.DocRoot)))
|
||||||
|
}
|
||||||
|
if versions := body.child("Versions"); versions != nil {
|
||||||
|
var replacement []byte
|
||||||
|
if entry == nil {
|
||||||
|
replacement = body.textXML("DocRoot", version.DocRoot)
|
||||||
|
}
|
||||||
|
patches = append(patches, editorXMLPatch{versions.start, versions.end, replacement})
|
||||||
|
} else if entry == nil {
|
||||||
|
return nil, fmt.Errorf("document entry not found: %d", r.documentIndex)
|
||||||
|
}
|
||||||
|
references := []*editorXML{body.child("Signatures")}
|
||||||
|
if info := body.child("DocInfo"); info != nil {
|
||||||
|
references = append(references, info.child("Cover"))
|
||||||
|
}
|
||||||
|
for _, node := range references {
|
||||||
|
if node == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := resolveResourcePath("OFD.xml", "", editorImportText(data, node))
|
||||||
|
if files[name] {
|
||||||
|
patches = append(patches, editorXMLContent(data, node, editorFontXMLText("/"+name)))
|
||||||
|
} else {
|
||||||
|
patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = editorPatchXML(data, patches)
|
||||||
|
if version.Version != "" {
|
||||||
|
root, err = parseEditorXMLContext(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err = editorXMLAttribute(data, root, "Version", version.Version)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = ctx.Err()
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
Reference in new issue
Block a user