mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
d07972346b
commit
70003486b8
37 files changed
+2386
-2509
No files matched your search
@@ -15,13 +15,147 @@
|
||||
package font
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 字体匹配按精确、部分和模糊匹配排列,零值表示未匹配
|
||||
const (
|
||||
MatchNone = iota
|
||||
MatchExact
|
||||
MatchPartial
|
||||
MatchFuzzy
|
||||
)
|
||||
|
||||
// matchRules 保存常用字体的名称、文件名及系统字体匹配规则
|
||||
var matchRules = normalizeMatchRules([]matchRule{
|
||||
{
|
||||
Keys: []string{"小标宋", "方正小标宋", "xiaobiaosong", "fzxiaobiaosong", "fzxbs"},
|
||||
Names: []string{"小标宋体", "方正小标宋简体", "FZXiaoBiaoSong-B05", "FZXiaoBiaoSong-B05S"},
|
||||
Files: []string{"FZXBSJW.TTF", "fzxbs*.ttf", "fzxiaobiaosong*.ttf", "xiaobiaosong*.ttf"},
|
||||
System: "FZXiaoBiaoSong-B05S",
|
||||
NoSyntheticBold: true,
|
||||
},
|
||||
{
|
||||
Keys: []string{"宋体", "新宋体", "simsun", "nsimsun", "simsunextb", "song", "songti", "stsong", "书宋", "方正书宋"},
|
||||
Names: []string{"宋体", "新宋体", "SimSun", "NSimSun", "SongTi", "STSong"},
|
||||
Files: []string{"simsun.ttc", "simsun.ttf", "nsimsun.ttf", "STSONG.TTF"},
|
||||
System: "SimSun",
|
||||
},
|
||||
{
|
||||
Keys: []string{"黑体", "hei", "simhei", "heiti", "sthei", "方正黑体"},
|
||||
Names: []string{"黑体", "SimHei", "HeiTi", "STHeiti", "方正黑体简体"},
|
||||
Files: []string{"simhei.ttf", "STXIHEI.TTF", "FZHT_GB18030.TTF", "fzht*.ttf"},
|
||||
System: "SimHei",
|
||||
},
|
||||
{
|
||||
Keys: []string{"楷体", "kai", "kaiti", "simkai", "stkaiti", "方正楷体"},
|
||||
Names: []string{"楷体", "KaiTi", "SimKai", "STKaiti", "方正楷体简体"},
|
||||
Files: []string{"simkai.ttf", "kaiti.ttf", "STKAITI.TTF", "FZKTJ.TTF", "fzkai*.ttf", "fzkt*.ttf"},
|
||||
System: "KaiTi",
|
||||
},
|
||||
{
|
||||
Keys: []string{"仿宋", "fang", "fangsong", "simfang", "stfangsong", "方正仿宋"},
|
||||
Names: []string{"仿宋", "FangSong", "SimFang", "STFangsong", "方正仿宋简体"},
|
||||
Files: []string{"simfang.ttf", "fangsong.ttf", "STFANGSO.TTF", "FZFSJ.TTF", "fzfs*.ttf"},
|
||||
System: "FangSong",
|
||||
},
|
||||
{
|
||||
Keys: []string{"微软雅黑", "microsoftyahei", "yahei", "msyh"},
|
||||
Names: []string{"微软雅黑", "Microsoft YaHei", "YaHei", "MSYH"},
|
||||
Files: []string{"msyh.ttc", "msyh.ttf"},
|
||||
System: "Microsoft YaHei",
|
||||
},
|
||||
{
|
||||
Keys: []string{"arialblack"},
|
||||
Names: []string{"Arial Black", "ArialBlack"},
|
||||
Files: []string{"ariblk.ttf", "arial black*.ttf", "arialblack*.ttf"},
|
||||
System: "Arial Black",
|
||||
},
|
||||
{
|
||||
Keys: []string{"arial"},
|
||||
Names: []string{"Arial"},
|
||||
Files: []string{"arial.ttf", "arial*.ttf"},
|
||||
System: "Arial",
|
||||
},
|
||||
{
|
||||
Keys: []string{"helvetica"},
|
||||
Names: []string{"Helvetica", "Arial"},
|
||||
Files: []string{"arial.ttf", "arial*.ttf"},
|
||||
System: "Arial",
|
||||
},
|
||||
{
|
||||
Keys: []string{"couriernew", "courier"},
|
||||
Names: []string{"Courier New", "CourierNew", "Courier"},
|
||||
Files: []string{"cour.ttf", "courier new*.ttf", "courier*.ttf"},
|
||||
System: "Courier New",
|
||||
},
|
||||
{
|
||||
Keys: []string{"segoeui"},
|
||||
Names: []string{"Segoe UI", "SegoeUI"},
|
||||
Files: []string{"segoeui.ttf", "segoe*.ttf"},
|
||||
System: "Segoe UI",
|
||||
},
|
||||
{
|
||||
Keys: []string{"timesnewroman", "times"},
|
||||
Names: []string{"Times New Roman", "TimesNewRoman"},
|
||||
Files: []string{"times.ttf", "times new roman*.ttf", "times*.ttf"},
|
||||
System: "Times New Roman",
|
||||
},
|
||||
})
|
||||
|
||||
// fontNameReplacer 移除字体名称匹配时忽略的分隔符
|
||||
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
|
||||
|
||||
// matchRule 字体匹配规则
|
||||
type matchRule struct {
|
||||
Keys []string
|
||||
Names []string
|
||||
Files []string
|
||||
System string
|
||||
NoSyntheticBold bool
|
||||
}
|
||||
|
||||
// FileCandidate 字体文件候选
|
||||
type FileCandidate struct {
|
||||
Name string
|
||||
Face int
|
||||
base string
|
||||
lowerBase string
|
||||
filename string
|
||||
normalized string
|
||||
names []string
|
||||
fold string
|
||||
}
|
||||
|
||||
// patternMatcher 字体文件匹配器
|
||||
type patternMatcher struct {
|
||||
pattern string
|
||||
lowerPattern string
|
||||
stem string
|
||||
aliases []string
|
||||
priority int
|
||||
}
|
||||
|
||||
// FileMatch 字体文件匹配结果
|
||||
type FileMatch struct {
|
||||
Name string
|
||||
Face int
|
||||
Priority int
|
||||
Rank int
|
||||
styleRank int
|
||||
sortName string
|
||||
}
|
||||
|
||||
// fileKey 字体文件与集合索引的去重键
|
||||
type fileKey struct {
|
||||
Name string
|
||||
Face int
|
||||
}
|
||||
|
||||
// NormalizeName 规范化字体名称
|
||||
// 入参: name 字体名称
|
||||
// 返回: string 规范化后的字体名称
|
||||
@@ -62,3 +196,611 @@ func AppendFontName(names []string, seen map[string]bool, name string) []string
|
||||
seen[key] = true
|
||||
return append(names, name)
|
||||
}
|
||||
|
||||
// normalizeMatchRules 预处理固定字体规则的匹配名称,保留候选名称及顺序
|
||||
// 入参: rules 字体匹配规则
|
||||
// 返回: []matchRule 预处理后的规则
|
||||
func normalizeMatchRules(rules []matchRule) []matchRule {
|
||||
for i := range rules {
|
||||
rule := &rules[i]
|
||||
keys := append(append(rule.Keys, rule.Names...), rule.System)
|
||||
rule.Keys = nil
|
||||
for _, key := range keys {
|
||||
key = NormalizeName(key)
|
||||
if key != "" && !slices.Contains(rule.Keys, key) {
|
||||
rule.Keys = append(rule.Keys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// CandidateNames 获取字体候选名称
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []string 字体候选名称
|
||||
func CandidateNames(names ...string) []string {
|
||||
return candidateNamesByLevel(MatchFuzzy, names...)
|
||||
}
|
||||
|
||||
// ExactCandidateNames 获取精确字体候选名称
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []string 精确字体候选名称
|
||||
func ExactCandidateNames(names ...string) []string {
|
||||
return candidateNamesByLevel(MatchExact, names...)
|
||||
}
|
||||
|
||||
// qualifiedNames 获取保留名称后缀的字体候选
|
||||
// 入参: name 字体名称
|
||||
// 返回: []string 保留后缀的候选名称
|
||||
func qualifiedNames(name string) []string {
|
||||
name = NormalizeName(name)
|
||||
key := NormalizeName(name)
|
||||
var names []string
|
||||
seen := make(map[string]bool)
|
||||
for _, rule := range matchRules {
|
||||
if ruleMatchLevel(rule, key) != MatchPartial {
|
||||
continue
|
||||
}
|
||||
prefix := ""
|
||||
for _, alias := range rule.Keys {
|
||||
if len(alias) > len(prefix) && strings.HasPrefix(name, alias) {
|
||||
prefix = alias
|
||||
}
|
||||
}
|
||||
suffix := strings.TrimPrefix(name, prefix)
|
||||
for _, alias := range append([]string{rule.System}, rule.Names...) {
|
||||
names = AppendFontName(names, seen, NormalizeName(alias)+suffix)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// candidateNamesByLevel 获取指定等级内的字体候选名称
|
||||
// 入参: maxLevel 最大匹配等级, names 字体名称列表
|
||||
// 返回: []string 字体候选名称
|
||||
func candidateNamesByLevel(maxLevel int, names ...string) []string {
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
var bases []string
|
||||
for _, name := range names {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
result = AppendFontName(result, seen, name)
|
||||
bases = append(bases, name)
|
||||
}
|
||||
levels := matchLevels(bases)
|
||||
for level := MatchExact; level <= maxLevel; level++ {
|
||||
for i := range bases {
|
||||
for j, rule := range matchRules {
|
||||
if levels[i*len(matchRules)+j] == level {
|
||||
result = AppendFontName(result, seen, rule.System)
|
||||
for _, item := range rule.Names {
|
||||
result = AppendFontName(result, seen, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// patternStem 获取字体匹配模式主干
|
||||
// 入参: pattern 字体文件匹配模式
|
||||
// 返回: string 字体匹配模式主干
|
||||
func patternStem(pattern string) string {
|
||||
stem := strings.TrimSuffix(pattern, "*")
|
||||
if IsFontFileName(stem) {
|
||||
stem = strings.TrimSuffix(stem, path.Ext(stem))
|
||||
}
|
||||
if index := strings.IndexAny(stem, "*?["); index >= 0 {
|
||||
stem = stem[:index]
|
||||
}
|
||||
return NormalizeName(stem)
|
||||
}
|
||||
|
||||
// FilePatterns 获取字体文件匹配模式
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []string 字体文件匹配模式
|
||||
func FilePatterns(names ...string) []string {
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
levels := matchLevels(names)
|
||||
for level := MatchExact; level <= MatchFuzzy; level++ {
|
||||
for _, name := range candidateNamesAtLevel(level, names, levels) {
|
||||
result = appendPattern(result, seen, name+"*")
|
||||
}
|
||||
for i := range names {
|
||||
for j, rule := range matchRules {
|
||||
if levels[i*len(matchRules)+j] == level {
|
||||
for _, item := range rule.Files {
|
||||
result = appendPattern(result, seen, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SystemNames 获取系统字体名称
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []string 系统字体名称
|
||||
func SystemNames(names ...string) []string {
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
for _, name := range names {
|
||||
for _, item := range candidateNamesByLevel(MatchPartial, name) {
|
||||
result = AppendFontName(result, seen, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// candidateNamesAtLevel 获取指定等级的字体候选名称
|
||||
// 入参: level 匹配等级, names 字体名称列表, levels 名称与规则的匹配等级
|
||||
// 返回: []string 字体候选名称
|
||||
func candidateNamesAtLevel(level int, names []string, levels []int) []string {
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
for i, name := range names {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if level == MatchExact {
|
||||
result = AppendFontName(result, seen, name)
|
||||
}
|
||||
for j, rule := range matchRules {
|
||||
if levels[i*len(matchRules)+j] == level {
|
||||
result = AppendFontName(result, seen, rule.System)
|
||||
for _, item := range rule.Names {
|
||||
result = AppendFontName(result, seen, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// DefaultSystemNames 获取默认系统字体名称
|
||||
// 返回: []string 默认系统字体名称
|
||||
func DefaultSystemNames() []string {
|
||||
return []string{
|
||||
"SimSun", "Microsoft YaHei", "SimHei", "KaiTi", "FangSong",
|
||||
"Noto Sans CJK SC", "Source Han Sans SC", "Arial", "Segoe UI", "Times New Roman",
|
||||
}
|
||||
}
|
||||
|
||||
// NoSyntheticBold 判断字体是否禁用合成粗体
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: bool 是否禁用合成粗体
|
||||
func NoSyntheticBold(names ...string) bool {
|
||||
for _, name := range names {
|
||||
name = NormalizeName(name)
|
||||
for _, rule := range matchRules {
|
||||
if !rule.NoSyntheticBold {
|
||||
continue
|
||||
}
|
||||
level := ruleMatchLevel(rule, name)
|
||||
if level >= MatchExact && level <= MatchPartial {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchLevels 按名称和规则顺序计算匹配等级
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: []int 名称与规则的匹配等级
|
||||
func matchLevels(names []string) []int {
|
||||
levels := make([]int, len(names)*len(matchRules))
|
||||
for i, name := range names {
|
||||
name = NormalizeName(name)
|
||||
for j, rule := range matchRules {
|
||||
levels[i*len(matchRules)+j] = ruleMatchLevel(rule, name)
|
||||
}
|
||||
}
|
||||
return levels
|
||||
}
|
||||
|
||||
// ruleMatchLevel 获取字体规则匹配等级
|
||||
// 入参: rule 字体匹配规则, name 规范化后的字体名称
|
||||
// 返回: int 匹配等级
|
||||
func ruleMatchLevel(rule matchRule, name string) int {
|
||||
if name == "" {
|
||||
return MatchNone
|
||||
}
|
||||
level := MatchNone
|
||||
for _, item := range rule.Keys {
|
||||
if name == item {
|
||||
return MatchExact
|
||||
}
|
||||
if strings.HasPrefix(name, item) {
|
||||
level = MatchPartial
|
||||
} else if level == MatchNone && strings.Contains(name, item) {
|
||||
level = MatchFuzzy
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
// appendPattern 追加字体文件匹配模式
|
||||
// 入参: patterns 匹配模式列表, seen 去重映射, pattern 匹配模式
|
||||
// 返回: []string 匹配模式列表
|
||||
func appendPattern(patterns []string, seen map[string]bool, pattern string) []string {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
return patterns
|
||||
}
|
||||
key := strings.ToLower(pattern)
|
||||
if seen[key] {
|
||||
return patterns
|
||||
}
|
||||
seen[key] = true
|
||||
return append(patterns, pattern)
|
||||
}
|
||||
|
||||
// AppendFileNames 建立各字体的内部名称与文件别名匹配候选
|
||||
// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称
|
||||
// 返回: []FileCandidate 字体候选列表
|
||||
func AppendFileNames(candidates []FileCandidate, index int, faces [][]string) []FileCandidate {
|
||||
file := candidates[index]
|
||||
replaced := false
|
||||
for face, names := range faces {
|
||||
file.Face = face
|
||||
file.names = make([]string, len(names))
|
||||
for i, name := range names {
|
||||
file.names[i] = NormalizeName(name)
|
||||
}
|
||||
for _, name := range file.names {
|
||||
file.normalized = name
|
||||
if !replaced {
|
||||
candidates[index] = file
|
||||
replaced = true
|
||||
} else {
|
||||
candidates = append(candidates, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
// CollectionIndex 按内部名称与样式选择集合项,未匹配时保留首项
|
||||
// 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: int 零起始字体索引
|
||||
func CollectionIndex(data []byte, names []string, bold, italic bool) int {
|
||||
return NameIndex(FileNames(bytes.NewReader(data)), names, bold, italic)
|
||||
}
|
||||
|
||||
// NameIndex 按名称与样式选择字体索引,未匹配时保留首项
|
||||
// 入参: faces 各字体名称, names 目标名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: int 零起始字体索引
|
||||
func NameIndex(faces [][]string, names []string, bold, italic bool) int {
|
||||
candidates := AppendFileNames([]FileCandidate{{}}, 0, faces)
|
||||
if matches := FileMatches(candidates, names, bold, italic); len(matches) > 0 {
|
||||
return matches[0].Face
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// FileMatches 匹配字体文件候选
|
||||
// 入参: candidates 字体候选, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []FileMatch 按优先级排列的字体匹配
|
||||
func FileMatches(candidates []FileCandidate, names []string, bold, italic bool) []FileMatch {
|
||||
matches := make([]FileMatch, 0, len(candidates))
|
||||
seen := make(map[fileKey]int, len(candidates))
|
||||
if len(names) == 0 {
|
||||
for _, name := range DefaultSystemNames() {
|
||||
matches = append(matches, FileMatches(candidates, []string{name}, bold, italic)...)
|
||||
}
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
return matches[i].Rank < matches[j].Rank
|
||||
})
|
||||
unique := matches[:0]
|
||||
for _, match := range matches {
|
||||
key := fileKey{strings.ToLower(match.Name), match.Face}
|
||||
if _, ok := seen[key]; !ok {
|
||||
seen[key] = len(unique)
|
||||
unique = append(unique, match)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
for i, name := range names {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
for j, alias := range append([]string{name}, qualifiedNames(name)...) {
|
||||
matcher := patternMatcher{stem: NormalizeName(alias), priority: (i-len(names))*2 + min(j, 1)}
|
||||
if matcher.stem == "" {
|
||||
continue
|
||||
}
|
||||
for _, file := range candidates {
|
||||
if rank := matcher.rankCandidate(file); rank == MatchExact {
|
||||
appendFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, matcher := range newPatternMatchers(FilePatterns(names...)) {
|
||||
for _, file := range candidates {
|
||||
rank := matcher.rankCandidate(file)
|
||||
appendFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
||||
}
|
||||
}
|
||||
sortFileMatches(matches)
|
||||
return matches
|
||||
}
|
||||
|
||||
// FileCandidates 筛选字体文件候选
|
||||
// 入参: names 文件名列表, base 文件名提取函数
|
||||
// 返回: []FileCandidate 字体文件候选列表
|
||||
func FileCandidates(names []string, base func(string) string) []FileCandidate {
|
||||
files := make([]FileCandidate, 0, len(names))
|
||||
for _, name := range names {
|
||||
if IsFontFileName(name) {
|
||||
baseName := base(name)
|
||||
normalized := NormalizeName(strings.TrimSuffix(baseName, path.Ext(baseName)))
|
||||
files = append(files, FileCandidate{
|
||||
Name: name,
|
||||
base: baseName,
|
||||
lowerBase: strings.ToLower(baseName),
|
||||
filename: normalized,
|
||||
normalized: normalized,
|
||||
fold: strings.ToLower(name),
|
||||
})
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// newPatternMatcher 创建字体文件匹配器
|
||||
// 入参: pattern 匹配模式
|
||||
// 返回: patternMatcher 字体文件匹配器
|
||||
func newPatternMatcher(pattern string) patternMatcher {
|
||||
matcher := patternMatcher{
|
||||
pattern: pattern,
|
||||
lowerPattern: strings.ToLower(pattern),
|
||||
stem: patternStem(pattern),
|
||||
}
|
||||
if matcher.stem != "" {
|
||||
for _, alias := range ExactCandidateNames(matcher.stem) {
|
||||
alias = NormalizeName(alias)
|
||||
if alias != "" {
|
||||
matcher.aliases = append(matcher.aliases, alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matcher
|
||||
}
|
||||
|
||||
// newPatternMatchers 创建字体文件匹配器列表
|
||||
// 入参: patterns 匹配模式列表
|
||||
// 返回: []patternMatcher 字体文件匹配器列表
|
||||
func newPatternMatchers(patterns []string) []patternMatcher {
|
||||
matchers := make([]patternMatcher, 0, len(patterns))
|
||||
for priority, pattern := range patterns {
|
||||
matcher := newPatternMatcher(pattern)
|
||||
matcher.priority = priority
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
return matchers
|
||||
}
|
||||
|
||||
// rankCandidate 获取字体文件候选匹配等级
|
||||
// 入参: file 字体文件候选
|
||||
// 返回: int 匹配等级
|
||||
func (m patternMatcher) rankCandidate(file FileCandidate) int {
|
||||
exactPath, _ := path.Match(m.pattern, file.base)
|
||||
lowerPath, _ := path.Match(m.lowerPattern, file.lowerBase)
|
||||
if m.stem == "" {
|
||||
if exactPath || lowerPath {
|
||||
return MatchFuzzy
|
||||
}
|
||||
return MatchNone
|
||||
}
|
||||
name := m.candidateName(file)
|
||||
if name == m.stem {
|
||||
return MatchExact
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if name == alias {
|
||||
return MatchExact
|
||||
}
|
||||
}
|
||||
if fileKnownStyleSuffix(m.styleSuffixNormalized(name)) {
|
||||
return MatchExact
|
||||
}
|
||||
if strings.HasPrefix(name, m.stem) {
|
||||
return MatchPartial
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.HasPrefix(name, alias) {
|
||||
return MatchPartial
|
||||
}
|
||||
}
|
||||
if exactPath || lowerPath || strings.Contains(name, m.stem) {
|
||||
return MatchFuzzy
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.Contains(name, alias) {
|
||||
return MatchFuzzy
|
||||
}
|
||||
}
|
||||
return MatchNone
|
||||
}
|
||||
|
||||
// candidateName 保留同族内部名称的区分,只有无关联时使用调用方文件别名
|
||||
// 入参: file 字体文件候选
|
||||
// 返回: string 用于统一匹配名称与样式的规范名称
|
||||
func (m patternMatcher) candidateName(file FileCandidate) string {
|
||||
if len(file.names) == 0 || file.base == "" || m.stem == "" {
|
||||
return file.normalized
|
||||
}
|
||||
for _, name := range file.names {
|
||||
if strings.HasPrefix(name, m.stem) {
|
||||
return file.normalized
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.HasPrefix(name, alias) {
|
||||
return file.normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
return file.filename
|
||||
}
|
||||
|
||||
// styleSuffixNormalized 获取规范字体名称的样式后缀
|
||||
// 入参: name 规范字体名称
|
||||
// 返回: string 样式后缀
|
||||
func (m patternMatcher) styleSuffixNormalized(name string) string {
|
||||
if m.stem == "" || name == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(name, m.stem) {
|
||||
return strings.TrimPrefix(name, m.stem)
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.HasPrefix(name, alias) {
|
||||
return strings.TrimPrefix(name, alias)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// appendFileMatch 追加字体文件匹配结果
|
||||
// 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
|
||||
func appendFileMatch(matches *[]FileMatch, seen map[fileKey]int, matcher patternMatcher, file FileCandidate, rank int, bold, italic bool) {
|
||||
if rank == MatchNone {
|
||||
return
|
||||
}
|
||||
suffix := matcher.styleSuffixNormalized(matcher.candidateName(file))
|
||||
if rank == MatchFuzzy {
|
||||
suffix = matcher.styleSuffixNormalized(NormalizeName(file.base))
|
||||
}
|
||||
next := FileMatch{
|
||||
Name: file.Name,
|
||||
Face: file.Face,
|
||||
Priority: matcher.priority,
|
||||
Rank: rank,
|
||||
styleRank: fileStyleRank(suffix, bold, italic),
|
||||
sortName: file.lowerBase,
|
||||
}
|
||||
key := fileKey{file.fold, file.Face}
|
||||
if index, ok := seen[key]; ok {
|
||||
if fileMatchLess(next, (*matches)[index]) {
|
||||
(*matches)[index] = next
|
||||
}
|
||||
return
|
||||
}
|
||||
seen[key] = len(*matches)
|
||||
*matches = append(*matches, next)
|
||||
}
|
||||
|
||||
// sortFileMatches 排序字体文件匹配结果
|
||||
// 入参: matches 匹配结果
|
||||
func sortFileMatches(matches []FileMatch) {
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
return fileMatchLess(matches[i], matches[j])
|
||||
})
|
||||
}
|
||||
|
||||
// fileMatchLess 判断字体文件匹配结果优先级
|
||||
// 入参: left 左侧匹配结果, right 右侧匹配结果
|
||||
// 返回: bool 左侧是否优先
|
||||
func fileMatchLess(left, right FileMatch) bool {
|
||||
if left.Priority != right.Priority {
|
||||
return left.Priority < right.Priority
|
||||
}
|
||||
if left.Rank != right.Rank {
|
||||
return left.Rank < right.Rank
|
||||
}
|
||||
if left.styleRank != right.styleRank {
|
||||
return left.styleRank < right.styleRank
|
||||
}
|
||||
return left.sortName < right.sortName
|
||||
}
|
||||
|
||||
// FileMatchNames 获取字体文件匹配名称
|
||||
// 入参: matches 匹配结果
|
||||
// 返回: []string 字体文件列表
|
||||
func FileMatchNames(matches []FileMatch) []string {
|
||||
names := make([]string, 0, len(matches))
|
||||
seen := make(map[string]bool, len(matches))
|
||||
for _, match := range matches {
|
||||
key := strings.ToLower(match.Name)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
names = append(names, match.Name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// fileStyleRank 获取字体文件样式匹配等级
|
||||
// 入参: suffix 样式后缀, bold 是否粗体, italic 是否斜体
|
||||
// 返回: int 样式匹配等级
|
||||
func fileStyleRank(suffix string, bold, italic bool) int {
|
||||
fileBold, fileItalic := fileStyleFromSuffix(suffix)
|
||||
rank := 0
|
||||
if fileBold != bold {
|
||||
rank += 2
|
||||
}
|
||||
if fileItalic != italic {
|
||||
rank += 2
|
||||
}
|
||||
if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fileRegularSuffix(suffix) {
|
||||
rank++
|
||||
}
|
||||
return rank
|
||||
}
|
||||
|
||||
// fileStyleFromSuffix 获取样式后缀对应的字体样式
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否粗体, bool 是否斜体
|
||||
func fileStyleFromSuffix(suffix string) (bool, bool) {
|
||||
bold := suffix == "b" ||
|
||||
suffix == "bd" ||
|
||||
suffix == "bold" ||
|
||||
suffix == "bi" ||
|
||||
suffix == "bolditalic" ||
|
||||
suffix == "boldoblique"
|
||||
italic := suffix == "i" ||
|
||||
suffix == "it" ||
|
||||
suffix == "italic" ||
|
||||
suffix == "oblique" ||
|
||||
suffix == "bi" ||
|
||||
suffix == "bolditalic" ||
|
||||
suffix == "boldoblique"
|
||||
return bold, italic
|
||||
}
|
||||
|
||||
// fileRegularSuffix 判断是否为常规样式后缀
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否为常规样式后缀
|
||||
func fileRegularSuffix(suffix string) bool {
|
||||
switch suffix {
|
||||
case "r", "regular", "normal", "常规":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// fileKnownStyleSuffix 判断是否为已知样式后缀
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否为已知样式后缀
|
||||
func fileKnownStyleSuffix(suffix string) bool {
|
||||
switch suffix {
|
||||
case "r", "regular", "normal", "常规",
|
||||
"b", "bd", "bold",
|
||||
"i", "it", "italic", "oblique",
|
||||
"bi", "bolditalic", "boldoblique":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package geom
|
||||
|
||||
import "math"
|
||||
|
||||
// polygonPoint 允许同结构的后端顶点直接参与裁剪,避免整片坐标转换
|
||||
type polygonPoint interface {
|
||||
~struct{ X, Y float64 }
|
||||
}
|
||||
|
||||
// ClipPolygon 按非退化凸窗口逐边裁剪多边形,保留原顶点绕向,不修改输入
|
||||
// 入参: a 绘制顶点, b 已通过ConvexPolygon检查的裁剪顶点,均不含重复闭合点
|
||||
// 返回: []P 交集顶点,少于三个顶点时不构成区域, bool 是否满足数值精度
|
||||
func ClipPolygon[P polygonPoint](a, b []P) ([]P, bool) {
|
||||
for i, start := range b {
|
||||
origin, end := Point(start), Point(b[(i+1)%len(b)])
|
||||
dx, dy := end.X-origin.X, end.Y-origin.Y
|
||||
length := math.Hypot(dx, dy)
|
||||
if length == 0 {
|
||||
continue
|
||||
}
|
||||
dx, dy = dx/length, dy/length
|
||||
distance := func(p P) float64 { return convexDistance(origin, Point(p), dx, dy) }
|
||||
sign := 1.0
|
||||
for _, p := range b {
|
||||
if d := distance(p); d != 0 {
|
||||
sign = math.Copysign(1, d)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(a) == 0 {
|
||||
break
|
||||
}
|
||||
var clipped []P
|
||||
appendPoint := func(p P) {
|
||||
if len(clipped) == 0 || p != clipped[len(clipped)-1] {
|
||||
clipped = append(clipped, p)
|
||||
}
|
||||
}
|
||||
previous := a[len(a)-1]
|
||||
previousDistance := sign * distance(previous)
|
||||
if !finite(previousDistance) {
|
||||
return nil, false
|
||||
}
|
||||
for _, p := range a {
|
||||
d := sign * distance(p)
|
||||
if !finite(d) {
|
||||
return nil, false
|
||||
}
|
||||
if (d >= 0) != (previousDistance >= 0) {
|
||||
scale := math.Max(math.Abs(d), math.Abs(previousDistance))
|
||||
fraction := (previousDistance / scale) / (previousDistance/scale - d/scale)
|
||||
intersection := Lerp(Point(previous), Point(p), fraction)
|
||||
if dx == 0 {
|
||||
intersection.X = origin.X
|
||||
}
|
||||
if dy == 0 {
|
||||
intersection.Y = origin.Y
|
||||
}
|
||||
appendPoint(P(intersection))
|
||||
}
|
||||
if d >= 0 {
|
||||
appendPoint(p)
|
||||
}
|
||||
previous, previousDistance = p, d
|
||||
}
|
||||
if len(clipped) > 1 && clipped[0] == clipped[len(clipped)-1] {
|
||||
clipped = clipped[:len(clipped)-1]
|
||||
}
|
||||
a = clipped
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
|
||||
// ConvexPolygon 检查所有顶点位于各边的同一侧,不修改输入
|
||||
// 入参: points 不含重复闭合点的独立顶点
|
||||
// 返回: []P 原顶点,非有限距离或异侧顶点返回nil, bool 是否为非退化凸多边形
|
||||
func ConvexPolygon[P polygonPoint](points []P) ([]P, bool) {
|
||||
positive := false
|
||||
for i, start := range points {
|
||||
origin, end := Point(start), Point(points[(i+1)%len(points)])
|
||||
dx, dy := end.X-origin.X, end.Y-origin.Y
|
||||
length := math.Hypot(dx, dy)
|
||||
if !finite(length) {
|
||||
return nil, false
|
||||
}
|
||||
if length == 0 {
|
||||
continue
|
||||
}
|
||||
dx, dy = dx/length, dy/length
|
||||
sign := 0.0
|
||||
for _, p := range points {
|
||||
d := convexDistance(origin, Point(p), dx, dy)
|
||||
if !finite(d) || sign*d < 0 {
|
||||
return nil, false
|
||||
}
|
||||
if d != 0 {
|
||||
sign, positive = math.Copysign(1, d), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return points, positive
|
||||
}
|
||||
|
||||
// convexDistance 按浮点乘法误差界识别共线点,不使用固定坐标容差
|
||||
// 入参: start 边起点, point 待测顶点, dx 横向单位分量, dy 纵向单位分量
|
||||
// 返回: float64 顶点到边的有向距离
|
||||
func convexDistance(start, point Point, dx, dy float64) float64 {
|
||||
x, y := point.X-start.X, point.Y-start.Y
|
||||
distance := math.FMA(dx, y, -dy*x)
|
||||
errorBound := (math.Abs(dx*y) + math.Abs(dy*x)) * 0x1p-49
|
||||
if finite(errorBound) && math.Abs(distance) <= errorBound {
|
||||
return 0
|
||||
}
|
||||
return distance
|
||||
}
|
||||
@@ -20,8 +20,20 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"image"
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// PNGDensityChunkSize 为PNG像素密度块占用的字节数
|
||||
const PNGDensityChunkSize = 21
|
||||
|
||||
// Density 保存横纵像素密度,Unit为0表示比例、1表示每米像素数
|
||||
type Density struct {
|
||||
X, Y float64
|
||||
Unit byte
|
||||
}
|
||||
|
||||
// PageInfo 图片方向与像素密度,不改变原始编码
|
||||
type PageInfo struct {
|
||||
Orientation int
|
||||
@@ -232,3 +244,179 @@ func imageExifMetadata(data []byte, dpi float64, directory uint32) (int, float64
|
||||
}
|
||||
return orientation, x, y, nil
|
||||
}
|
||||
|
||||
// BMPDensity 提取已校验BMP头中的每米像素数
|
||||
// 入参: data 含至少40字节信息头的BMP编码
|
||||
// 返回: Density 像素密度, bool 密度是否可保留
|
||||
func BMPDensity(data []byte) (Density, bool) {
|
||||
xdpi, ydpi := int32(binary.LittleEndian.Uint32(data[38:42])), int32(binary.LittleEndian.Uint32(data[42:46]))
|
||||
if xdpi < 0 || ydpi < 0 {
|
||||
return Density{}, false
|
||||
}
|
||||
return Density{X: float64(xdpi), Y: float64(ydpi), Unit: 1}, true
|
||||
}
|
||||
|
||||
// TIFFConversionDensity 校验可转换的TIFF字段并提取规范化后的像素密度
|
||||
// 色彩配置、预乘透明度、扩展字段及其他目录需保留原始编码
|
||||
// 入参: data TIFF编码, layout 已校验的主图布局
|
||||
// 返回: Density PNG像素密度, bool 是否可完整转换
|
||||
func TIFFConversionDensity(data []byte, layout TIFFLayout) (Density, bool) {
|
||||
var density Density
|
||||
var order binary.ByteOrder = binary.LittleEndian
|
||||
if data[0] == 'M' {
|
||||
order = binary.BigEndian
|
||||
}
|
||||
if layout.Next != 0 || layout.Offset != order.Uint32(data[4:]) || layout.Reduced {
|
||||
return density, false
|
||||
}
|
||||
offset := int(layout.Offset)
|
||||
count := int(order.Uint16(data[offset:]))
|
||||
unit := uint32(2)
|
||||
var resolutions [2][2]uint32
|
||||
for i := range count {
|
||||
entry := data[offset+2+i*12 : offset+14+i*12]
|
||||
tag, kind, number := order.Uint16(entry), order.Uint16(entry[2:]), order.Uint32(entry[4:])
|
||||
value := order.Uint32(entry[8:])
|
||||
if kind == 3 && number == 1 {
|
||||
value = uint32(order.Uint16(entry[8:]))
|
||||
}
|
||||
switch tag {
|
||||
case 256, 257, 258, 259, 273, 274, 277, 278, 279, 320, 322, 323, 324, 325:
|
||||
case 262:
|
||||
if value == 5 {
|
||||
return density, false
|
||||
}
|
||||
case 254:
|
||||
if value != 0 {
|
||||
return density, false
|
||||
}
|
||||
case 255:
|
||||
if value != 1 {
|
||||
return density, false
|
||||
}
|
||||
case 266:
|
||||
if kind != 3 || number != 1 || value != 1 && value != 2 {
|
||||
return density, false
|
||||
}
|
||||
case 284:
|
||||
if kind != 3 || number != 1 || value != 1 && value != 2 {
|
||||
return density, false
|
||||
}
|
||||
case 292, 293:
|
||||
mask := uint32(7)
|
||||
if tag == 293 {
|
||||
mask = 2
|
||||
}
|
||||
if kind != 4 || number != 1 || value & ^mask != 0 {
|
||||
return density, false
|
||||
}
|
||||
case 317:
|
||||
if kind != 3 || number != 1 || value != 1 && value != 2 {
|
||||
return density, false
|
||||
}
|
||||
case 338:
|
||||
if kind != 3 || number != 1 || value != 2 {
|
||||
return density, false
|
||||
}
|
||||
case 339:
|
||||
if kind != 3 || number == 0 || number > 4 {
|
||||
return density, false
|
||||
}
|
||||
values := entry[8:]
|
||||
if number > 2 {
|
||||
if uint64(value)+uint64(number)*2 > uint64(len(data)) {
|
||||
return density, false
|
||||
}
|
||||
values = data[value:]
|
||||
}
|
||||
for j := uint32(0); j < number; j++ {
|
||||
if order.Uint16(values[j*2:]) != 1 {
|
||||
return density, false
|
||||
}
|
||||
}
|
||||
case 282, 283:
|
||||
if kind != 5 || number != 1 || uint64(value)+8 > uint64(len(data)) {
|
||||
return density, false
|
||||
}
|
||||
resolutions[tag-282] = [2]uint32{order.Uint32(data[value:]), order.Uint32(data[value+4:])}
|
||||
case 296:
|
||||
if kind != 3 || number != 1 || value < 1 || value > 3 {
|
||||
return density, false
|
||||
}
|
||||
unit = value
|
||||
default:
|
||||
return density, false
|
||||
}
|
||||
}
|
||||
if resolutions == [2][2]uint32{} {
|
||||
return density, true
|
||||
}
|
||||
x, y := resolutions[0], resolutions[1]
|
||||
if x[0] == 0 || x[1] == 0 || y[0] == 0 || y[1] == 0 {
|
||||
return density, false
|
||||
}
|
||||
if unit == 1 {
|
||||
a, b := uint64(x[0])*uint64(y[1]), uint64(y[0])*uint64(x[1])
|
||||
divisor, next := a, b
|
||||
for next != 0 {
|
||||
divisor, next = next, divisor%next
|
||||
}
|
||||
density.X, density.Y = float64(a/divisor), float64(b/divisor)
|
||||
} else {
|
||||
factor := 1 / 0.0254
|
||||
if unit == 3 {
|
||||
factor = 100
|
||||
}
|
||||
density = Density{X: float64(x[0]) / float64(x[1]) * factor, Y: float64(y[0]) / float64(y[1]) * factor, Unit: 1}
|
||||
}
|
||||
if math.Round(density.X) < 1 || math.Round(density.Y) < 1 || math.Round(density.X) > math.MaxInt32 || math.Round(density.Y) > math.MaxInt32 {
|
||||
return density, false
|
||||
}
|
||||
if layout.Orientation >= 5 {
|
||||
density.X, density.Y = density.Y, density.X
|
||||
}
|
||||
return density, true
|
||||
}
|
||||
|
||||
// Scale 按最终像素尺寸保留绝对密度或精确相对比例
|
||||
// 入参: before 原始像素尺寸, after 最终像素尺寸
|
||||
// 返回: Density 缩放后密度, bool 相对比例是否可完整保留
|
||||
func (density Density) Scale(before, after image.Point) (Density, bool) {
|
||||
hasDensity := density.X != 0 || density.Y != 0
|
||||
if hasDensity && before != after {
|
||||
if density.Unit == 0 {
|
||||
ratio := new(big.Rat).SetFrac64(int64(density.X)*int64(after.X), int64(density.Y)*int64(after.Y))
|
||||
ratio.Mul(ratio, new(big.Rat).SetFrac64(int64(before.Y), int64(before.X)))
|
||||
if ratio.Num().BitLen() > 31 || ratio.Denom().BitLen() > 31 {
|
||||
return density, false
|
||||
}
|
||||
density.X, density.Y = float64(ratio.Num().Int64()), float64(ratio.Denom().Int64())
|
||||
} else {
|
||||
density.X *= float64(after.X) / float64(before.X)
|
||||
density.Y *= float64(after.Y) / float64(before.Y)
|
||||
}
|
||||
}
|
||||
return density, true
|
||||
}
|
||||
|
||||
// WithPNGDensity 在无密度块的PNG头后插入pHYs,保留源编码不变
|
||||
// 入参: data 完整PNG编码, density 像素密度
|
||||
// 返回: []byte 带密度的编码, bool 密度是否可完整保留
|
||||
func WithPNGDensity(data []byte, density Density) ([]byte, bool) {
|
||||
x, y := math.Round(density.X), math.Round(density.Y)
|
||||
if x > math.MaxInt32 || y > math.MaxInt32 || density.X > 0 && x < 1 || density.Y > 0 && y < 1 {
|
||||
return nil, false
|
||||
}
|
||||
chunk := make([]byte, 21)
|
||||
binary.BigEndian.PutUint32(chunk, 9)
|
||||
copy(chunk[4:], "pHYs")
|
||||
binary.BigEndian.PutUint32(chunk[8:], uint32(x))
|
||||
binary.BigEndian.PutUint32(chunk[12:], uint32(y))
|
||||
chunk[16] = density.Unit
|
||||
binary.BigEndian.PutUint32(chunk[17:], crc32.ChecksumIEEE(chunk[4:17]))
|
||||
output := make([]byte, 0, len(data)+PNGDensityChunkSize)
|
||||
output = append(output, data[:33]...)
|
||||
output = append(output, chunk...)
|
||||
output = append(output, data[33:]...)
|
||||
return output, true
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
package image
|
||||
|
||||
import (
|
||||
"image"
|
||||
@@ -21,8 +21,12 @@ import (
|
||||
"slices"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/math/f64"
|
||||
)
|
||||
|
||||
// rasterScaleBufferLimit 限制整像素缩放器的浮点中间缓冲,超出时使用逐行采样
|
||||
const rasterScaleBufferLimit = 8 << 20
|
||||
|
||||
// rasterAxisSample 保存目标像素及其归一化源采样权重
|
||||
type rasterAxisSample struct {
|
||||
pixel int
|
||||
@@ -55,7 +59,7 @@ func (s rasterImageSource) RGBA64At(x, y int) color.RGBA64 {
|
||||
// drawRasterAxisImage 按像素中心进行轴向缩小,逐行复用横向采样并保留镜像与小数坐标
|
||||
// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作
|
||||
// 返回: 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])
|
||||
if m[1] != 0 || m[2] != 0 || sx == 0 || sy == 0 || sx > 1 || sy > 1 || sx == 1 && sy == 1 {
|
||||
return false
|
||||
@@ -69,7 +73,7 @@ func drawRasterAxisImage(dst draw.Image, src image.Image, m RasterMatrix, op dra
|
||||
if b.Empty() {
|
||||
return true
|
||||
}
|
||||
r := rasterImageBounds(b, m, dst.Bounds())
|
||||
r := TransformBounds(b, m, dst.Bounds())
|
||||
if r.Empty() {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"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输出,与保留的设备四色对象使用相同显示换算
|
||||
// 入参: space 合成空间, values 非预乘分量
|
||||
// 返回: [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)
|
||||
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 (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||
)
|
||||
|
||||
// Action 动作
|
||||
@@ -548,3 +549,92 @@ func resolveActionURI(action URI) 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"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
|
||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// rasterDensity 保存横纵像素密度,unit为0表示比例、1表示每米像素数
|
||||
type rasterDensity struct {
|
||||
x, y float64
|
||||
unit byte
|
||||
}
|
||||
|
||||
// outputImageFormat 识别可由资源压缩生成的图片格式
|
||||
// 入参: data 图片编码
|
||||
// 返回: string 标准资源格式,未识别时为空
|
||||
@@ -96,8 +87,8 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
|
||||
if int64(offset)+stride*int64(height) != int64(len(data)) {
|
||||
return data, nil
|
||||
}
|
||||
xdpi, ydpi := int32(binary.LittleEndian.Uint32(data[38:42])), int32(binary.LittleEndian.Uint32(data[42:46]))
|
||||
if xdpi < 0 || ydpi < 0 {
|
||||
density, ok := baseimage.BMPDensity(data)
|
||||
if !ok {
|
||||
return data, nil
|
||||
}
|
||||
if depth == 16 || depth == 32 {
|
||||
@@ -122,7 +113,7 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
|
||||
if err != nil {
|
||||
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 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果
|
||||
@@ -130,8 +121,8 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
|
||||
// 有密度时保留PNG,未知密度按最终像素选择编码格式
|
||||
// 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度
|
||||
// 返回: []byte 图片编码, error 编码或取消错误
|
||||
func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density rasterDensity) ([]byte, error) {
|
||||
hasDensity := density.x != 0 || density.y != 0
|
||||
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
|
||||
before := pixels.Bounds().Size()
|
||||
if options.Mode == CompressionLossy {
|
||||
var err error
|
||||
@@ -140,18 +131,10 @@ func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if after := pixels.Bounds().Size(); 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 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 ok bool
|
||||
density, ok = density.Scale(before, pixels.Bounds().Size())
|
||||
if !ok {
|
||||
return data, ctx.Err()
|
||||
}
|
||||
var candidate []byte
|
||||
var err error
|
||||
@@ -169,23 +152,13 @@ func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image
|
||||
return nil, err
|
||||
}
|
||||
if hasDensity {
|
||||
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 || len(candidate) > outputOptimizationBufferLimit-21 {
|
||||
if len(candidate) > outputOptimizationBufferLimit-baseimage.PNGDensityChunkSize {
|
||||
return data, ctx.Err()
|
||||
}
|
||||
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]))
|
||||
var output bytes.Buffer
|
||||
writer := imageMetadataWriter{writer: &output, skip: 33, data: chunk}
|
||||
if _, err := writer.Write(candidate); err != nil {
|
||||
return nil, err
|
||||
candidate, ok = baseimage.WithPNGDensity(candidate, density)
|
||||
if !ok {
|
||||
return data, ctx.Err()
|
||||
}
|
||||
candidate = output.Bytes()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
@@ -195,3 +168,30 @@ func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
@@ -258,3 +261,162 @@ func editorActionsXML(actions []Action) ([]byte, error) {
|
||||
node := root.child("Actions")
|
||||
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"
|
||||
"context"
|
||||
"fmt"
|
||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||
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
|
||||
}
|
||||
|
||||
// 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}
|
||||
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 {
|
||||
file, err = e.resolveBindingFont(ctx, scan, declaration)
|
||||
@@ -415,7 +415,7 @@ func originalBindingFont(ctx context.Context, scan *editorFontDocument, declarat
|
||||
}
|
||||
index := 0
|
||||
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)
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
|
||||
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"unicode/utf8"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
loaders map[string]func() ([]byte, error)
|
||||
names []string
|
||||
candidates []fontFileCandidate
|
||||
}
|
||||
|
||||
// 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
|
||||
candidates []basefont.FileCandidate
|
||||
}
|
||||
|
||||
// fontMemFile 内存字体文件
|
||||
@@ -130,9 +93,9 @@ func NewFontFS(fonts []FontFile) *FontFS {
|
||||
fsys.files[name] = append([]byte(nil), font.Data...)
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
@@ -159,7 +122,7 @@ func NewLazyFontFS(names []string, load func(string) ([]byte, error)) *FontFS {
|
||||
})
|
||||
}
|
||||
sort.Strings(fsys.names)
|
||||
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
|
||||
fsys.candidates = basefont.FileCandidates(fsys.names, path.Base)
|
||||
return fsys
|
||||
}
|
||||
|
||||
@@ -238,8 +201,8 @@ func (fsys *FontFS) Match(names ...string) (string, bool) {
|
||||
// 返回: string 匹配字体文件, bool 是否为名称匹配
|
||||
func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool) {
|
||||
if len(names) > 0 {
|
||||
if matches := fsys.matchStyle(names, bold, italic); len(matches) > 0 {
|
||||
return matches[0].name, true
|
||||
if matches := basefont.FileMatches(fsys.candidates, names, bold, italic); len(matches) > 0 {
|
||||
return matches[0].Name, true
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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 按文档字体名称和样式选择首选文件,未声明的使用编号按默认规则回退
|
||||
// 入参: files 可用字体文件名, ids 已发现的使用编号,省略时仅匹配声明字体
|
||||
// 返回: []string 所需字体文件名, error 错误信息
|
||||
@@ -268,19 +224,19 @@ func (r *Reader) MatchFontFiles(files []string, ids ...string) ([]string, error)
|
||||
fonts = append(fonts, Font{ID: id})
|
||||
}
|
||||
}
|
||||
candidates := fontFileCandidates(files, path.Base)
|
||||
candidates := basefont.FileCandidates(files, path.Base)
|
||||
names := make([]string, 0, len(fonts))
|
||||
seen := make(map[string]bool)
|
||||
for _, font := range fonts {
|
||||
if font.FontFile != "" {
|
||||
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 {
|
||||
matches = fontFileMatches(candidates, nil, font.Bold, font.Italic)
|
||||
matches = basefont.FileMatches(candidates, nil, font.Bold, font.Italic)
|
||||
}
|
||||
if len(matches) > 0 {
|
||||
name := matches[0].name
|
||||
name := matches[0].Name
|
||||
if !seen[name] {
|
||||
names = append(names, name)
|
||||
seen[name] = true
|
||||
@@ -405,68 +361,12 @@ func (i fontFileInfo) Sys() any {
|
||||
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 获取回退字体文件
|
||||
// 入参: bold 是否粗体, italic 是否斜体
|
||||
// 返回: []string 字体文件列表
|
||||
func (fsys *FontFS) fallbackFonts(bold, italic bool) []string {
|
||||
if matches := fsys.matchStyle(nil, bold, italic); len(matches) > 0 {
|
||||
return fontFileMatchNames(matches)
|
||||
if matches := basefont.FileMatches(fsys.candidates, nil, bold, italic); len(matches) > 0 {
|
||||
return basefont.FileMatchNames(matches)
|
||||
}
|
||||
if len(fsys.names) > 0 {
|
||||
return []string{fsys.names[0]}
|
||||
@@ -502,284 +402,3 @@ func cleanFontName(name string) string {
|
||||
}
|
||||
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
|
||||
}
|
||||
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 data, nil
|
||||
@@ -268,7 +268,7 @@ func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) {
|
||||
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 {
|
||||
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 保存字体候选及独立字体数据,原始集合按字节预算淘汰,不包含绘图库对象
|
||||
type fontSourceCache struct {
|
||||
candidates map[string][]fontSource
|
||||
directories map[string][]fontFileCandidate
|
||||
filesystems map[int][]fontFileCandidate
|
||||
directories map[string][]basefont.FileCandidate
|
||||
filesystems map[int][]basefont.FileCandidate
|
||||
data map[fontSourceKey]fontSourceData
|
||||
collections *renderCache[fontSourceKey, []byte]
|
||||
fallback ResolvedFont
|
||||
@@ -81,7 +81,7 @@ type fontSourceData struct {
|
||||
// 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 复用相同来源的文件索引与数据,匹配和回退状态保持独立
|
||||
@@ -99,11 +99,11 @@ func (c *fontSourceCache) child() *fontSourceCache {
|
||||
func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) {
|
||||
key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name}
|
||||
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 {
|
||||
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 {
|
||||
return cached.data, cached.err
|
||||
@@ -192,7 +192,7 @@ func (r *Renderer) resolveFontSource(backend FontBackend, id string, exact bool)
|
||||
if !r.fontSourcesCache.fallbackRead {
|
||||
r.fontSourcesCache.fallbackRead = true
|
||||
if canLoadSystemFonts() {
|
||||
for _, name := range fontDefaultSystemNames() {
|
||||
for _, name := range basefont.DefaultSystemNames() {
|
||||
data, err := r.readFontSource(fontSource{kind: fontSourceSystem, name: name}, &Font{})
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -221,12 +221,12 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
||||
seen := make(map[fontSourceKey]bool)
|
||||
for _, dir := range r.fontDirs {
|
||||
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 _, 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)
|
||||
@@ -234,10 +234,10 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
||||
if canLoadSystemFonts() {
|
||||
for _, dir := range systemFontDirs() {
|
||||
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}
|
||||
for i, requested := range names {
|
||||
if basefont.NormalizeName(name) == basefont.NormalizeName(requested) {
|
||||
@@ -251,12 +251,12 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
||||
sortFontSources(sources[systemStart:])
|
||||
for _, dir := range r.fontDirs {
|
||||
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 _, 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 {
|
||||
@@ -294,39 +294,39 @@ func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source
|
||||
|
||||
// matchFontFiles 查找字体文件
|
||||
// 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []fontFileMatch 字体匹配列表
|
||||
func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []fontFileMatch {
|
||||
// 返回: []basefont.FileMatch 字体匹配列表
|
||||
func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []basefont.FileMatch {
|
||||
candidates, ok := r.fontSourcesCache.directories[dir]
|
||||
if !ok {
|
||||
files, _ := filepath.Glob(filepath.Join(dir, "*"))
|
||||
candidates = fontFileCandidates(files, filepath.Base)
|
||||
candidates = basefont.FileCandidates(files, filepath.Base)
|
||||
for i, candidate := range candidates {
|
||||
file, err := os.Open(candidate.name)
|
||||
file, err := os.Open(candidate.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
candidates = appendFontFileNames(candidates, i, basefont.FileNames(file))
|
||||
candidates = basefont.AppendFileNames(candidates, i, basefont.FileNames(file))
|
||||
file.Close()
|
||||
}
|
||||
r.fontSourcesCache.directories[dir] = candidates
|
||||
}
|
||||
return fontFileMatches(candidates, names, bold, italic)
|
||||
return basefont.FileMatches(candidates, names, bold, italic)
|
||||
}
|
||||
|
||||
// matchFontFS 匹配字体文件系统中的字体名称
|
||||
// 入参: index 字体文件系统索引, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []fontFileMatch 字体匹配列表
|
||||
func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []fontFileMatch {
|
||||
// 返回: []basefont.FileMatch 字体匹配列表
|
||||
func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []basefont.FileMatch {
|
||||
fsys := r.fontFS[index]
|
||||
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]
|
||||
if !ok {
|
||||
files, _ := fs.Glob(fsys, "*")
|
||||
candidates = fontFileCandidates(files, path.Base)
|
||||
candidates = basefont.FileCandidates(files, path.Base)
|
||||
for i, candidate := range candidates {
|
||||
file, err := fsys.Open(candidate.name)
|
||||
file, err := fsys.Open(candidate.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -335,12 +335,12 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f
|
||||
data, _ := io.ReadAll(file)
|
||||
reader = bytes.NewReader(data)
|
||||
}
|
||||
candidates = appendFontFileNames(candidates, i, basefont.FileNames(reader))
|
||||
candidates = basefont.AppendFileNames(candidates, i, basefont.FileNames(reader))
|
||||
file.Close()
|
||||
}
|
||||
r.fontSourcesCache.filesystems[index] = candidates
|
||||
}
|
||||
return fontFileMatches(candidates, names, bold, italic)
|
||||
return basefont.FileMatches(candidates, names, bold, italic)
|
||||
}
|
||||
|
||||
// systemFontDirs 获取系统字体目录
|
||||
@@ -378,8 +378,8 @@ func systemFontDirs() []string {
|
||||
}
|
||||
|
||||
// indexSystemFonts 递归读取系统字体名称,保留集合索引
|
||||
// 返回: []fontFileCandidate 字体候选
|
||||
func indexSystemFonts() []fontFileCandidate {
|
||||
// 返回: []basefont.FileCandidate 字体候选
|
||||
func indexSystemFonts() []basefont.FileCandidate {
|
||||
var files []string
|
||||
seen := make(map[string]bool)
|
||||
for _, dir := range systemFontDirs() {
|
||||
@@ -391,13 +391,13 @@ func indexSystemFonts() []fontFileCandidate {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
candidates := fontFileCandidates(files, filepath.Base)
|
||||
candidates := basefont.FileCandidates(files, filepath.Base)
|
||||
for i, candidate := range candidates {
|
||||
file, err := os.Open(candidate.name)
|
||||
file, err := os.Open(candidate.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
candidates = appendFontFileNames(candidates, i, basefont.FileNames(file))
|
||||
candidates = basefont.AppendFileNames(candidates, i, basefont.FileNames(file))
|
||||
file.Close()
|
||||
}
|
||||
return candidates
|
||||
|
||||
+132
@@ -23,6 +23,9 @@ import (
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// pdfObjectChunkLimit 限制导入暂存片段的容量,不限制页面对象数量
|
||||
const pdfObjectChunkLimit = 256
|
||||
|
||||
// ErrPDFPassword 表示PDF需要正确的用户或所有者密码
|
||||
var ErrPDFPassword = pdfgo.ErrPassword
|
||||
|
||||
@@ -118,6 +121,12 @@ type pdfImporter struct {
|
||||
compression CompressionOptions
|
||||
}
|
||||
|
||||
// pdfObjectBuffer 分块收集导入对象,提交前只整理一次连续数据
|
||||
type pdfObjectBuffer struct {
|
||||
parts [][]GraphicObject
|
||||
size int
|
||||
}
|
||||
|
||||
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
||||
// 入参: ctx 取消上下文, source PDF数据, size 字节数, options 转换选项
|
||||
// 返回: *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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
|
||||
type pdfCompositePixel struct {
|
||||
values [4]float64
|
||||
alpha float64
|
||||
effect float64
|
||||
shape float64
|
||||
}
|
||||
// pdfCompositePixel 复用内部PDF像素布局
|
||||
type pdfCompositePixel = codecpdf.CompositePixel
|
||||
|
||||
// pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元
|
||||
type pdfCompositor struct {
|
||||
@@ -121,7 +116,7 @@ type pdfCompositeCache struct {
|
||||
shadingPatterns map[*pdfgo.ShadingPattern][]pdfCompositeNode
|
||||
shadingSources map[pdfShadingSourceKey][]pdfCompositeNode
|
||||
shadings map[pdfGradientKey]pdfCompositeGeometry
|
||||
transfers map[pdfTransferKey]*pdfgo.TransferFunction
|
||||
transfers codecpdf.TransferCache
|
||||
textDisjoint map[*pdfgo.GroupMark]bool
|
||||
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)
|
||||
if !externalNames[basefont.NormalizeName(name)] {
|
||||
for _, candidate := range fontExactCandidateNames(name) {
|
||||
for _, candidate := range basefont.ExactCandidateNames(name) {
|
||||
externalNames[basefont.NormalizeName(candidate)] = true
|
||||
}
|
||||
}
|
||||
@@ -907,7 +902,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
||||
return err
|
||||
}
|
||||
for i := range pixels {
|
||||
pixels[i].effect = 0
|
||||
pixels[i].Effect = 0
|
||||
}
|
||||
copy(previousTransfers, c.transfers)
|
||||
if err := c.draw([]pdfCompositeNode{node}, pixels, space); err != nil {
|
||||
@@ -919,16 +914,16 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
||||
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
|
||||
}
|
||||
var rgb [3]float64
|
||||
var err error
|
||||
if c.transfers != nil && device == "DeviceCMYK" {
|
||||
values := pixel.values
|
||||
values := pixel.Values
|
||||
if flatten {
|
||||
for j := range values {
|
||||
values[j] *= pixel.alpha
|
||||
values[j] *= pixel.Alpha
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
} else if c.transfers != nil {
|
||||
rgb, err = space.RGB(pixel.values[:space.Components()], "RelativeColorimetric")
|
||||
rgb, err = space.RGB(pixel.Values[:space.Components()], "RelativeColorimetric")
|
||||
} else {
|
||||
rgb, err = codecpdf.OutputColor(space, pixel.values)
|
||||
rgb, err = codecpdf.OutputColor(space, pixel.Values)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alpha := pixel.alpha
|
||||
alpha := pixel.Alpha
|
||||
if flatten {
|
||||
if c.transfers == nil || device != "DeviceCMYK" {
|
||||
for j := range rgb {
|
||||
rgb[j] = pixel.alpha*rgb[j] + 1 - pixel.alpha
|
||||
rgb[j] = pixel.Alpha*rgb[j] + 1 - pixel.Alpha
|
||||
}
|
||||
}
|
||||
alpha = 1
|
||||
|
||||
+25
-147
@@ -15,9 +15,7 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||
@@ -84,8 +82,8 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
|
||||
defer c.releasePixels(result)
|
||||
copy(result, pixels)
|
||||
for i := range result {
|
||||
result[i].effect = 0
|
||||
result[i].shape = 0
|
||||
result[i].Effect = 0
|
||||
result[i].Shape = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +107,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -266,14 +264,14 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
if patternChannels != nil && patternChannels[index]&pdfTransferShape == 0 {
|
||||
selectionShape = 0
|
||||
}
|
||||
values = sample.values
|
||||
values = sample.Values
|
||||
if paint.Shading != nil {
|
||||
shape = sample.shape
|
||||
shape = sample.Shape
|
||||
} else {
|
||||
shape *= sample.shape
|
||||
shape *= sample.Shape
|
||||
}
|
||||
if sample.shape != 0 {
|
||||
opacity *= sample.alpha / sample.shape
|
||||
if sample.Shape != 0 {
|
||||
opacity *= sample.Alpha / sample.Shape
|
||||
}
|
||||
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})
|
||||
@@ -359,22 +357,22 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
}
|
||||
if initial != nil {
|
||||
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
|
||||
}
|
||||
if err := pdfCompositeInterpolate(&pixels[index], pixel, shape); err != nil {
|
||||
if err := codecpdf.CompositeInterpolate(&pixels[index], pixel, shape); err != nil {
|
||||
return err
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
pixels[index].shape += shape * (1 - pixels[index].shape)
|
||||
pixels[index].Shape += shape * (1 - pixels[index].Shape)
|
||||
bits := pdfTransferChannels
|
||||
if (c.channels != nil || transfer != nil && c.transferModel == "DeviceCMYK") && (compatible || marked != nil || patternChannels != nil) {
|
||||
bits = 0
|
||||
for j, selected := range pdfPaintChannels(values, compatible, marked) {
|
||||
for j, selected := range codecpdf.PaintChannels(values, compatible, marked) {
|
||||
if selected {
|
||||
bits |= 1 << j
|
||||
}
|
||||
@@ -409,18 +407,6 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
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 按隔离标志保留初始背景,移除背景贡献后应用组透明度
|
||||
// 非隔离同空间的非挖空组借用父组背景,子组只写入独立结果缓冲
|
||||
// 入参: node 透明组, pixels 父组像素, parent 父混合空间
|
||||
@@ -481,7 +467,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
||||
}
|
||||
}
|
||||
pixel := &pixels[i]
|
||||
values := pixel.values
|
||||
values := pixel.Values
|
||||
if !sameSpace {
|
||||
var err error
|
||||
values, err = space.Convert(values[:components], parent, "RelativeColorimetric")
|
||||
@@ -489,7 +475,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
||||
return err
|
||||
}
|
||||
}
|
||||
background[i] = pdfCompositePixel{values: values, alpha: pixel.alpha}
|
||||
background[i] = pdfCompositePixel{Values: values, Alpha: pixel.Alpha}
|
||||
}
|
||||
}
|
||||
if !sharedInitial {
|
||||
@@ -526,13 +512,13 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
||||
return err
|
||||
}
|
||||
}
|
||||
result[i].shape *= g.Alpha
|
||||
result[i].Shape *= g.Alpha
|
||||
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
|
||||
}
|
||||
if c.channels != nil {
|
||||
@@ -542,7 +528,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
||||
return err
|
||||
}
|
||||
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]
|
||||
if independent {
|
||||
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 x := region.Min.X; x < region.Max.X; 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
|
||||
}
|
||||
if c.channels != nil && candidate[i].shape != 0 {
|
||||
if c.channels != nil && candidate[i].Shape != 0 {
|
||||
c.channels[i] = local.channels[i]
|
||||
}
|
||||
}
|
||||
@@ -618,76 +604,6 @@ func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels [
|
||||
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 蒙版或空值, inherited 未指定蒙版空间时继承的空间
|
||||
// 返回: []float64 蒙版透明度或空值, error 蒙版错误
|
||||
@@ -724,8 +640,8 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
copy(pixels[i].values[:], mask.Backdrop)
|
||||
pixels[i].alpha = 1
|
||||
copy(pixels[i].Values[:], mask.Backdrop)
|
||||
pixels[i].Alpha = 1
|
||||
}
|
||||
}
|
||||
local := *c
|
||||
@@ -748,9 +664,9 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
v := pixel.alpha
|
||||
v := pixel.Alpha
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -917,41 +833,3 @@ func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, e
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha}
|
||||
result[i] = pdfCompositePixel{Values: pixel.Values, Alpha: pixel.Alpha}
|
||||
}
|
||||
candidate := c.acquirePixels(len(backdrop))
|
||||
defer c.releasePixels(candidate)
|
||||
@@ -113,7 +113,7 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
||||
if disjoint {
|
||||
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 {
|
||||
cellChannels[y*width+x] = 0
|
||||
if !disjoint {
|
||||
@@ -136,7 +136,7 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
||||
for x := range width {
|
||||
index := (top+y)*c.width + left + x
|
||||
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 {
|
||||
channels[index] |= cellChannels[y*width+x]
|
||||
} else {
|
||||
@@ -154,12 +154,12 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
||||
}
|
||||
}
|
||||
pixel := &result[i]
|
||||
if pixel.effect != 0 {
|
||||
if pixel.Effect != 0 {
|
||||
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
|
||||
return result, nil
|
||||
@@ -168,23 +168,23 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
||||
// pdfCompositePatternCell 合并互不重叠单元的覆盖贡献,避免相邻抗锯齿边缘重复混合
|
||||
// 入参: target 图案累计结果, initial 原始背景, source 单元结果, disjoint 单元边界是否互不重叠
|
||||
func pdfCompositePatternCell(target *pdfCompositePixel, initial, source pdfCompositePixel, disjoint bool) {
|
||||
if source.shape == 0 {
|
||||
if source.Shape == 0 {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
for i := range target.values {
|
||||
target.values[i] = (target.values[i]*target.alpha + source.values[i]*source.alpha - initial.values[i]*initial.alpha) / alpha
|
||||
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.alpha = alpha
|
||||
target.effect = math.Min(1, target.effect+source.effect)
|
||||
target.shape = math.Min(1, target.shape+source.shape)
|
||||
target.Alpha = alpha
|
||||
target.Effect = math.Min(1, target.Effect+source.Effect)
|
||||
target.Shape = math.Min(1, target.Shape+source.Shape)
|
||||
} else {
|
||||
target.values, target.alpha = source.values, source.alpha
|
||||
target.effect += source.effect * (1 - target.effect)
|
||||
target.shape += source.shape * (1 - target.shape)
|
||||
target.Values, target.Alpha = source.Values, source.Alpha
|
||||
target.Effect += source.Effect * (1 - target.Effect)
|
||||
target.Shape += source.Shape * (1 - target.Shape)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,17 +114,17 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo
|
||||
}
|
||||
remaining := 1.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 {
|
||||
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].shape += background[i].shape * remaining
|
||||
if channels != nil && background[i].shape*remaining > 0 {
|
||||
result[i].Alpha, result[i].Effect = alpha, alpha
|
||||
result[i].Shape += background[i].Shape * remaining
|
||||
if channels != nil && background[i].Shape*remaining > 0 {
|
||||
channels[i] |= backgroundChannels[i]
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func (c *pdfCompositor) shadingSourcePixels(node pdfCompositeNode, backdrop []pd
|
||||
}
|
||||
}()
|
||||
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 {
|
||||
return nil, err
|
||||
@@ -202,12 +202,12 @@ func (c *pdfCompositor) shadingSourcePixels(node pdfCompositeNode, backdrop []pd
|
||||
}
|
||||
}
|
||||
pixel := &result[i]
|
||||
if pixel.effect != 0 {
|
||||
if pixel.Effect != 0 {
|
||||
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
|
||||
return result, nil
|
||||
|
||||
+2
-42
@@ -25,38 +25,14 @@ const (
|
||||
pdfTransferShape uint8 = 16
|
||||
)
|
||||
|
||||
// pdfTransferKey 区分图形状态、网屏覆盖函数及输出设备
|
||||
type pdfTransferKey struct {
|
||||
transfer *pdfgo.TransferFunction
|
||||
halftone *pdfgo.Halftone
|
||||
model pdfgo.Name
|
||||
}
|
||||
|
||||
// resolveTransfer 在页面内复用输出设备的有效函数,不修改原始图形状态
|
||||
// resolveTransfer 无设备函数时保留无缓存快速路径,按需绑定单页解析缓存
|
||||
// 入参: style 原始图形状态, model 原生设备颜色空间
|
||||
// 返回: *pdfgo.TransferFunction 有效函数, error 网屏传递函数错误
|
||||
func (p *pdfImporter) resolveTransfer(style pdfgo.Style, model pdfgo.Name) (*pdfgo.TransferFunction, error) {
|
||||
if style.Halftone == nil && style.Transfer == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if 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
|
||||
return p.compositingCache().transfers.Resolve(p.reader, style, model)
|
||||
}
|
||||
|
||||
// compositeHasTransfer 检查最终设备使用的通道,不将备用通道误判为有效函数
|
||||
@@ -134,22 +110,6 @@ func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pd
|
||||
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 复用图案不透明性检查,图案内部函数不提前作用于源颜色
|
||||
// 返回: bool 是否允许使用外层设备函数, error 单元解析错误
|
||||
func (p *pdfCompositePattern) opaqueTransfer() (bool, error) {
|
||||
|
||||
+3
-70
@@ -16,16 +16,11 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
|
||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/math/f64"
|
||||
)
|
||||
|
||||
// rasterScaleBufferLimit 限制整像素缩放器的浮点中间缓冲,超出时使用逐行采样
|
||||
const rasterScaleBufferLimit = 8 << 20
|
||||
|
||||
// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
|
||||
// 入参: m 局部到页面变换, height 目标像素高度
|
||||
// 返回: RasterMatrix 局部到像素变换
|
||||
@@ -38,56 +33,13 @@ func (p *RasterPage) PixelTransform(m RasterMatrix, height int) RasterMatrix {
|
||||
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 使用当前后端生成裁剪蒙版,共用高质量图像采样
|
||||
// 入参: backend 当前后端, page 页面, dst 目标图像, src 原始图像, command 图像指令, cache 蒙版缓存
|
||||
// 返回: 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())
|
||||
if command.Clip == nil {
|
||||
drawRasterImage(dst, src, m)
|
||||
baseimage.DrawPixels(dst, src, m, draw.Over)
|
||||
return nil
|
||||
}
|
||||
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() {
|
||||
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() {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
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 (
|
||||
"fmt"
|
||||
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||
"math"
|
||||
"strings"
|
||||
@@ -133,7 +134,7 @@ func (r *Renderer) positionText(object TextObject, state RenderState, contentOnl
|
||||
bold := weight >= 700
|
||||
if definition := r.Reader.fontDefinition(id); definition != nil {
|
||||
bold = bold && !definition.Bold
|
||||
if definition.FontFile == "" && fontNoSyntheticBold(definition.FontName, definition.FamilyName) {
|
||||
if definition.FontFile == "" && basefont.NoSyntheticBold(definition.FontName, definition.FamilyName) {
|
||||
bold = false
|
||||
}
|
||||
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"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||
)
|
||||
|
||||
// clipRenderer 收集图形和文字的绘制轮廓
|
||||
@@ -262,6 +263,117 @@ func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
|
||||
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 合并裁剪区域
|
||||
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
|
||||
// 返回: *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
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
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.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"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
||||
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
|
||||
if of := r.Reader.fontDefinition(fontID); of != nil {
|
||||
embeddedFont = of.FontFile != ""
|
||||
if syntheticBold && !embeddedFont && fontNoSyntheticBold(of.FontName, of.FamilyName) {
|
||||
if syntheticBold && !embeddedFont && basefont.NoSyntheticBold(of.FontName, of.FamilyName) {
|
||||
syntheticBold = false
|
||||
}
|
||||
if of.Bold {
|
||||
|
||||
+103
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -340,3 +341,105 @@ func textCodeRunesContext(ctx context.Context, value string) ([]rune, error) {
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
@@ -153,3 +154,157 @@ func (r *Reader) versionSignatures(location string) string {
|
||||
}
|
||||
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