style(代码风格): 整理声明顺序与字体模块
build.yaml / build (push) Successful in 51s

This commit is contained in:
xiaoqidun committed 2026-09-28 15:17:45 +08:00
1 parent 5e0da3da28
commit ee6c9f6a16
82 files changed
+4316 -4310

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20260928063322-fedbd99f692e github.com/xiaoqidun/pdfgo v0.0.0-20260928071431-2842b88597d4
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0 golang.org/x/image v0.46.0
golang.org/x/text v0.42.0 golang.org/x/text v0.42.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20260928063322-fedbd99f692e h1:5Y8EEWAf0+eIQAZ02ma3p8Ws0kbtEz9czobV0qAybU4= github.com/xiaoqidun/pdfgo v0.0.0-20260928071431-2842b88597d4 h1:R0jk+8LHdihgZdzvLHJQgX1ptkPfiK45rSvb7i9L3Fw=
github.com/xiaoqidun/pdfgo v0.0.0-20260928063322-fedbd99f692e/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xiaoqidun/pdfgo v0.0.0-20260928071431-2842b88597d4/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+35 -35
View File
@@ -42,6 +42,21 @@ import (
"github.com/xiaoqidun/ofdgo" "github.com/xiaoqidun/ofdgo"
) )
// currentSession 当前WebUI文档会话
var currentSession *Session
// currentEditor 当前编辑文档
var currentEditor *ofdgo.Editor
// pendingImport 待插页文档,仅保留文件数据与索引,不创建渲染会话
var pendingImport *ofdgo.Reader
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据
var copiedObjects *editorClipboard
// copiedStyle 当前文档中的独立样式快照
var copiedStyle *ofdgo.GraphicObject
// exportWriter 分块传递导出数据 // exportWriter 分块传递导出数据
type exportWriter struct { type exportWriter struct {
write js.Value write js.Value
@@ -115,21 +130,6 @@ type annotationOptions struct {
Area *ofdgo.Box Area *ofdgo.Box
} }
// currentSession 当前WebUI文档会话
var currentSession *Session
// currentEditor 当前编辑文档
var currentEditor *ofdgo.Editor
// pendingImport 待插页文档,仅保留文件数据与索引,不创建渲染会话
var pendingImport *ofdgo.Reader
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据
var copiedObjects *editorClipboard
// copiedStyle 当前文档中的独立样式快照
var copiedStyle *ofdgo.GraphicObject
// Write 将数据块交给浏览器保存 // Write 将数据块交给浏览器保存
// 入参: data 导出数据 // 入参: data 导出数据
// 返回: int 写入长度, error 错误信息 // 返回: int 写入长度, error 错误信息
@@ -184,26 +184,6 @@ func (r *browserReaderAt) ReadAt(data []byte, offset int64) (int, error) {
return n, nil return n, nil
} }
// awaitExport 等待浏览器处理导出检查点
// 入参: fn 浏览器回调, args 回调参数
// 返回: error 写入错误或取消原因
func awaitExport(fn js.Value, args ...any) error {
done := make(chan error)
callback := js.FuncOf(func(this js.Value, args []js.Value) any {
var err error
if args[1].Bool() {
err = context.Canceled
} else if message := args[0].String(); message != "" {
err = fmt.Errorf("%s", message)
}
done <- err
return nil
})
defer callback.Release()
fn.Invoke(append(args, callback)...)
return <-done
}
// RunWASM 注册浏览器WASM接口并阻塞运行 // RunWASM 注册浏览器WASM接口并阻塞运行
func RunWASM() { func RunWASM() {
registerCallback("ofdgoOpen", openDocument) registerCallback("ofdgoOpen", openDocument)
@@ -291,6 +271,26 @@ func RunWASM() {
select {} select {}
} }
// awaitExport 等待浏览器处理导出检查点
// 入参: fn 浏览器回调, args 回调参数
// 返回: error 写入错误或取消原因
func awaitExport(fn js.Value, args ...any) error {
done := make(chan error)
callback := js.FuncOf(func(this js.Value, args []js.Value) any {
var err error
if args[1].Bool() {
err = context.Canceled
} else if message := args[0].String(); message != "" {
err = fmt.Errorf("%s", message)
}
done <- err
return nil
})
defer callback.Release()
fn.Invoke(append(args, callback)...)
return <-done
}
// registerCallback 注册浏览器回调函数 // registerCallback 注册浏览器回调函数
// 入参: name 回调名称, fn 回调函数 // 入参: name 回调名称, fn 回调函数
func registerCallback(name string, fn func([]js.Value) (any, error)) { func registerCallback(name string, fn func([]js.Value) (any, error)) {
+162 -162
View File
@@ -266,38 +266,6 @@ func Open(data []byte, opts OpenOptions) (*Session, error) {
return newSession(reader, opts) return newSession(reader, opts)
} }
// newSession 从文档快照创建阅读会话
// 入参: reader 文档读取器, opts 打开选项
// 返回: *Session 文档会话, error 错误信息
func newSession(reader *ofdgo.Reader, opts OpenOptions) (*Session, error) {
doc, err := reader.Doc()
if err != nil {
reader.Close()
return nil, err
}
var rendererOptions []ofdgo.RendererOption
var fontFS *ofdgo.FontFS
if len(opts.Fonts) > 0 {
fontFS = ofdgo.NewFontFS(opts.Fonts)
if fontFS.Len() == 0 {
reader.Close()
return nil, fmt.Errorf("invalid font file")
}
rendererOptions = append(rendererOptions, ofdgo.WithFontFS(fontFS))
}
rendererOptions = append(rendererOptions, ofdgo.WithAnnotations(opts.RenderAnnotations))
return &Session{
Reader: reader,
Renderer: ofdgo.NewRenderer(reader, rendererOptions...),
fontFS: fontFS,
doc: doc,
pageCache: make(map[int]*ofdgo.PageContent),
boxCache: make([]pageBoxInfo, len(doc.Pages.Page)),
textCache: make(map[int]*ofdgo.PageText),
svgFonts: make(map[string][]byte),
}, nil
}
// Close 关闭文档会话 // Close 关闭文档会话
// 返回: error 错误信息 // 返回: error 错误信息
func (s *Session) Close() error { func (s *Session) Close() error {
@@ -333,59 +301,6 @@ func (s *Session) SetFonts(fonts []FontFile) error {
return nil return nil
} }
// resetFontInfo 清除字体统计及扫描状态,供字体或注解配置变更后重算
func (s *Session) resetFontInfo() {
s.fontScan = nil
s.fontInfos = nil
s.fontsRead = false
}
// scanFontInfo 分步统计字体,每次最多处理一页,完成后保留当前会话的诊断
// 返回: bool 是否完成
func (s *Session) scanFontInfo() bool {
if s.fontAnnotations != s.Renderer.RenderAnnotations {
s.resetFontInfo()
s.fontAnnotations = s.Renderer.RenderAnnotations
}
if s.fontsRead {
return true
}
if s.fontScan == nil {
var err error
s.fontScan, err = s.Renderer.ScanFontInfos()
if err != nil {
s.fontsRead = true
return true
}
}
if s.fontScan.Next() {
return false
}
s.fontInfos, _ = s.fontScan.Infos()
s.fontsRead = true
s.fontScan = nil
return true
}
// scanFontInfoBatch 为大文档批量统计字体,限制每次处理的页数与耗时
// 返回: bool 是否完成
func (s *Session) scanFontInfoBatch() bool {
limit := 1
if len(s.doc.Pages.Page) > 256 {
limit = 64
}
start := time.Now()
for range limit {
if s.scanFontInfo() {
return true
}
if time.Since(start) >= 8*time.Millisecond {
break
}
}
return false
}
// PageText 获取指定页面文字,复用当前会话的文字索引 // PageText 获取指定页面文字,复用当前会话的文字索引
// 入参: index 页面索引 // 入参: index 页面索引
// 返回: *ofdgo.PageText 页面文字, error 错误信息 // 返回: *ofdgo.PageText 页面文字, error 错误信息
@@ -417,16 +332,6 @@ func (s *Session) PageTextString(index int) (string, error) {
return text.String(), nil return text.String(), nil
} }
// readPageText 复用已加载页面,否则只解析文字所需图元
// 入参: index 页面索引
// 返回: *ofdgo.PageText 页面文字, error 错误信息
func (s *Session) readPageText(index int) (*ofdgo.PageText, error) {
if page := s.pageCache[index]; page != nil {
return s.Renderer.PageText(page)
}
return s.Renderer.PageTextByIndex(index)
}
// SearchPage 搜索指定页面,复用当前会话的文字索引 // SearchPage 搜索指定页面,复用当前会话的文字索引
// 入参: index 页面索引, query 搜索文字 // 入参: index 页面索引, query 搜索文字
// 返回: []ofdgo.TextMatch 匹配结果, error 错误信息 // 返回: []ofdgo.TextMatch 匹配结果, error 错误信息
@@ -479,23 +384,6 @@ func (s *Session) Summary() DocumentInfo {
return info return info
} }
// pageInfos 获取全部真实页面尺寸,首屏、配置及详情共用区域缓存
// 返回: []PageInfo 独立页面信息列表
func (s *Session) pageInfos() []PageInfo {
pages := make([]PageInfo, len(s.doc.Pages.Page))
for index, page := range s.doc.Pages.Page {
cached := s.boxCache[index]
box := cached.box
if !cached.valid {
if area, err := s.Reader.PageArea(page); err == nil {
box, _ = s.pageBox(index, &ofdgo.PageContent{Area: area})
}
}
pages[index] = PageInfo{Index: index, ID: page.ID, Width: box.W, Height: box.H}
}
return pages
}
// Details 获取无需逐页扫描的附件、签名及字体匹配结果 // Details 获取无需逐页扫描的附件、签名及字体匹配结果
// 返回: DocumentDetails 文档补充信息 // 返回: DocumentDetails 文档补充信息
func (s *Session) Details() DocumentDetails { func (s *Session) Details() DocumentDetails {
@@ -629,62 +517,12 @@ func (s *Session) SVGFontData(name string) ([]byte, error) {
return data, nil return data, nil
} }
// pageContent 读取页面,复用最近一次解析结果
// 入参: index 页面索引
// 返回: *ofdgo.PageContent 页面内容, error 错误信息
func (s *Session) pageContent(index int) (*ofdgo.PageContent, error) {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if page, ok := s.pageCache[index]; ok {
return page, nil
}
clear(s.pageCache)
page, err := s.Reader.PageContentByIndex(index)
if err != nil {
return nil, err
}
s.pageCache[index] = page
return page, nil
}
// pageBox 获取页面物理区域
// 入参: index 页面索引, page 页面内容
// 返回: ofdgo.Box 页面物理区域, error 错误信息
func (s *Session) pageBox(index int, page *ofdgo.PageContent) (ofdgo.Box, error) {
if cached := s.boxCache[index]; cached.valid {
return cached.box, nil
}
box, err := s.Renderer.GetPageBox(page)
if err != nil {
return ofdgo.Box{}, err
}
s.boxCache[index] = pageBoxInfo{box: box, valid: true}
return box, nil
}
// ExportFormats 获取导出格式 // ExportFormats 获取导出格式
// 返回: []ExportFormat 导出格式列表 // 返回: []ExportFormat 导出格式列表
func ExportFormats() []ExportFormat { func ExportFormats() []ExportFormat {
return ofdgo.OutputFormats()[1:] return ofdgo.OutputFormats()[1:]
} }
// exportFormat 获取导出格式
// 入参: value 格式值
// 返回: ExportFormat 导出格式, bool 是否支持
func exportFormat(value string) (ExportFormat, bool) {
value = strings.ToLower(strings.TrimSpace(value))
if value == "jpeg" {
value = "jpg"
}
for _, format := range ExportFormats() {
if format.Value == value {
return format, true
}
}
return ExportFormat{}, false
}
// ExportPage 导出单页 // ExportPage 导出单页
// 入参: index 页面索引, value 导出格式, dpi 图片DPI, writer 输出流 // 入参: index 页面索引, value 导出格式, dpi 图片DPI, writer 输出流
// 返回: ExportFormat 导出格式, error 错误信息 // 返回: ExportFormat 导出格式, error 错误信息
@@ -739,6 +577,168 @@ func (s *Session) ExportPDF(writer io.Writer, indices ...int) error {
return s.Renderer.RenderToMultiPagePDF(writer, indices...) return s.Renderer.RenderToMultiPagePDF(writer, indices...)
} }
// newSession 从文档快照创建阅读会话
// 入参: reader 文档读取器, opts 打开选项
// 返回: *Session 文档会话, error 错误信息
func newSession(reader *ofdgo.Reader, opts OpenOptions) (*Session, error) {
doc, err := reader.Doc()
if err != nil {
reader.Close()
return nil, err
}
var rendererOptions []ofdgo.RendererOption
var fontFS *ofdgo.FontFS
if len(opts.Fonts) > 0 {
fontFS = ofdgo.NewFontFS(opts.Fonts)
if fontFS.Len() == 0 {
reader.Close()
return nil, fmt.Errorf("invalid font file")
}
rendererOptions = append(rendererOptions, ofdgo.WithFontFS(fontFS))
}
rendererOptions = append(rendererOptions, ofdgo.WithAnnotations(opts.RenderAnnotations))
return &Session{
Reader: reader,
Renderer: ofdgo.NewRenderer(reader, rendererOptions...),
fontFS: fontFS,
doc: doc,
pageCache: make(map[int]*ofdgo.PageContent),
boxCache: make([]pageBoxInfo, len(doc.Pages.Page)),
textCache: make(map[int]*ofdgo.PageText),
svgFonts: make(map[string][]byte),
}, nil
}
// resetFontInfo 清除字体统计及扫描状态,供字体或注解配置变更后重算
func (s *Session) resetFontInfo() {
s.fontScan = nil
s.fontInfos = nil
s.fontsRead = false
}
// scanFontInfo 分步统计字体,每次最多处理一页,完成后保留当前会话的诊断
// 返回: bool 是否完成
func (s *Session) scanFontInfo() bool {
if s.fontAnnotations != s.Renderer.RenderAnnotations {
s.resetFontInfo()
s.fontAnnotations = s.Renderer.RenderAnnotations
}
if s.fontsRead {
return true
}
if s.fontScan == nil {
var err error
s.fontScan, err = s.Renderer.ScanFontInfos()
if err != nil {
s.fontsRead = true
return true
}
}
if s.fontScan.Next() {
return false
}
s.fontInfos, _ = s.fontScan.Infos()
s.fontsRead = true
s.fontScan = nil
return true
}
// scanFontInfoBatch 为大文档批量统计字体,限制每次处理的页数与耗时
// 返回: bool 是否完成
func (s *Session) scanFontInfoBatch() bool {
limit := 1
if len(s.doc.Pages.Page) > 256 {
limit = 64
}
start := time.Now()
for range limit {
if s.scanFontInfo() {
return true
}
if time.Since(start) >= 8*time.Millisecond {
break
}
}
return false
}
// readPageText 复用已加载页面,否则只解析文字所需图元
// 入参: index 页面索引
// 返回: *ofdgo.PageText 页面文字, error 错误信息
func (s *Session) readPageText(index int) (*ofdgo.PageText, error) {
if page := s.pageCache[index]; page != nil {
return s.Renderer.PageText(page)
}
return s.Renderer.PageTextByIndex(index)
}
// pageInfos 获取全部真实页面尺寸,首屏、配置及详情共用区域缓存
// 返回: []PageInfo 独立页面信息列表
func (s *Session) pageInfos() []PageInfo {
pages := make([]PageInfo, len(s.doc.Pages.Page))
for index, page := range s.doc.Pages.Page {
cached := s.boxCache[index]
box := cached.box
if !cached.valid {
if area, err := s.Reader.PageArea(page); err == nil {
box, _ = s.pageBox(index, &ofdgo.PageContent{Area: area})
}
}
pages[index] = PageInfo{Index: index, ID: page.ID, Width: box.W, Height: box.H}
}
return pages
}
// pageContent 读取页面,复用最近一次解析结果
// 入参: index 页面索引
// 返回: *ofdgo.PageContent 页面内容, error 错误信息
func (s *Session) pageContent(index int) (*ofdgo.PageContent, error) {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if page, ok := s.pageCache[index]; ok {
return page, nil
}
clear(s.pageCache)
page, err := s.Reader.PageContentByIndex(index)
if err != nil {
return nil, err
}
s.pageCache[index] = page
return page, nil
}
// pageBox 获取页面物理区域
// 入参: index 页面索引, page 页面内容
// 返回: ofdgo.Box 页面物理区域, error 错误信息
func (s *Session) pageBox(index int, page *ofdgo.PageContent) (ofdgo.Box, error) {
if cached := s.boxCache[index]; cached.valid {
return cached.box, nil
}
box, err := s.Renderer.GetPageBox(page)
if err != nil {
return ofdgo.Box{}, err
}
s.boxCache[index] = pageBoxInfo{box: box, valid: true}
return box, nil
}
// exportFormat 获取导出格式
// 入参: value 格式值
// 返回: ExportFormat 导出格式, bool 是否支持
func exportFormat(value string) (ExportFormat, bool) {
value = strings.ToLower(strings.TrimSpace(value))
if value == "jpeg" {
value = "jpg"
}
for _, format := range ExportFormats() {
if format.Value == value {
return format, true
}
}
return ExportFormat{}, false
}
// encryptionInfo 获取当前文档的加密来源,不包含凭据 // encryptionInfo 获取当前文档的加密来源,不包含凭据
// 返回: EncryptionInfo 加密状态 // 返回: EncryptionInfo 加密状态
func (s *Session) encryptionInfo() EncryptionInfo { func (s *Session) encryptionInfo() EncryptionInfo {
+15 -15
View File
@@ -126,21 +126,6 @@ type actionSource struct {
Path string Path string
} }
// gotoDest 获取文档内跳转目标
// 入参: action 跳转动作, bookmarks 书签
// 返回: *Dest 跳转目标
func gotoDest(action *Goto, bookmarks map[string]Dest) *Dest {
if action.Dest != nil {
return action.Dest
}
if action.Bookmark != nil {
if dest, ok := bookmarks[action.Bookmark.Name]; ok {
return &dest
}
}
return nil
}
// UnmarshalXML 解析跳转目标 // UnmarshalXML 解析跳转目标
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
@@ -242,6 +227,21 @@ func (a *RegionArea) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
} }
} }
// gotoDest 获取文档内跳转目标
// 入参: action 跳转动作, bookmarks 书签
// 返回: *Dest 跳转目标
func gotoDest(action *Goto, bookmarks map[string]Dest) *Dest {
if action.Dest != nil {
return action.Dest
}
if action.Bookmark != nil {
if dest, ok := bookmarks[action.Bookmark.Name]; ok {
return &dest
}
}
return nil
}
// actionFloatAttr 获取动作浮点属性 // actionFloatAttr 获取动作浮点属性
// 入参: start 起始节点, name 属性名 // 入参: start 起始节点, name 属性名
// 返回: float64 属性值 // 返回: float64 属性值
+46 -46
View File
@@ -22,6 +22,13 @@ import (
"reflect" "reflect"
) )
const (
SVGEmbedded SVGMode = iota
SVGExternalFonts
SVGExternalResources
SVGObjects
)
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现 // ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
var ErrBackendUnavailable = errors.New("render backend unavailable") var ErrBackendUnavailable = errors.New("render backend unavailable")
@@ -68,13 +75,6 @@ type ObjectMeasurement struct {
// SVGMode 指定SVG资源封装方式,Objects模式还需保留稳定的对象分组 // SVGMode 指定SVG资源封装方式,Objects模式还需保留稳定的对象分组
type SVGMode uint8 type SVGMode uint8
const (
SVGEmbedded SVGMode = iota
SVGExternalFonts
SVGExternalResources
SVGObjects
)
// SVGBackend 输出SVG及资源,不得修改源页面或省略不支持的内容 // SVGBackend 输出SVG及资源,不得修改源页面或省略不支持的内容
// renderer提供文档和排版能力,输出时不得再次调用RenderToSVG系列方法 // renderer提供文档和排版能力,输出时不得再次调用RenderToSVG系列方法
type SVGBackend interface { type SVGBackend interface {
@@ -133,16 +133,6 @@ func (b RenderBackends) Info() BackendInfo {
return BackendInfo{FontResources: backendName(b.FontResources), Fonts: backendName(b.Fonts), Geometry: backendName(b.Geometry), Compiler: backendName(b.Compiler), Raster: backendName(b.Raster), SVG: backendName(b.SVG), PDF: backendName(b.PDF), EPS: backendName(b.EPS)} return BackendInfo{FontResources: backendName(b.FontResources), Fonts: backendName(b.Fonts), Geometry: backendName(b.Geometry), Compiler: backendName(b.Compiler), Raster: backendName(b.Raster), SVG: backendName(b.SVG), PDF: backendName(b.PDF), EPS: backendName(b.EPS)}
} }
// backendName 获取已配置的后端标识
// 入参: backend 后端实例
// 返回: string 后端标识,未配置时为空
func backendName(backend Backend) string {
if backend == nil {
return ""
}
return backend.Name()
}
// WithRenderBackends 替换完整后端组合,未配置的能力不会沿用默认实现 // WithRenderBackends 替换完整后端组合,未配置的能力不会沿用默认实现
// 入参: backends 后端组合 // 入参: backends 后端组合
// 返回: RendererOption 渲染选项 // 返回: RendererOption 渲染选项
@@ -167,6 +157,45 @@ func WithRenderBackends(backends RenderBackends) RendererOption {
} }
} }
// Backends 返回当前后端配置副本,可修改后通过WithRenderBackends应用
// 返回: RenderBackends 后端组合
func (r *Renderer) Backends() RenderBackends {
return r.backends
}
// WithRasterBackend 选择当前渲染器的图像、PNG和JPEG后端,不改变SVG、PDF或OFD保存
// 入参: backend 光栅后端,nil禁用图像输出
// 返回: RendererOption 渲染选项
func WithRasterBackend(backend RasterBackend) RendererOption {
return func(r *Renderer) {
if !sameBackend(r.backends.Raster, backend) {
r.backendStates = make(map[any]any)
}
r.backends.Raster = backend
}
}
// CompilePage 编译与后端无关的光栅页面,可交给任意RasterBackend绘制
// 字形、描边、裁剪和底纹沿用文档语义,不交由后端重新排版
// 入参: page 页面内容
// 返回: *RasterPage 只读绘制页面, error 编译错误
func (r *Renderer) CompilePage(page *PageContent) (*RasterPage, error) {
if r.backends.Compiler == nil {
return nil, fmt.Errorf("page compiler: %w", ErrBackendUnavailable)
}
return r.backends.Compiler.CompilePage(r, page)
}
// backendName 获取已配置的后端标识
// 入参: backend 后端实例
// 返回: string 后端标识,未配置时为空
func backendName(backend Backend) string {
if backend == nil {
return ""
}
return backend.Name()
}
// sameBackend 判断不可变值配置是否未变,包含指针的配置重新应用时刷新可变状态 // sameBackend 判断不可变值配置是否未变,包含指针的配置重新应用时刷新可变状态
// 入参: left 原后端, right 新后端 // 入参: left 原后端, right 新后端
// 返回: bool 是否为同一配置 // 返回: bool 是否为同一配置
@@ -202,32 +231,3 @@ func immutableBackendValue(value reflect.Value) bool {
} }
return true return true
} }
// Backends 返回当前后端配置副本,可修改后通过WithRenderBackends应用
// 返回: RenderBackends 后端组合
func (r *Renderer) Backends() RenderBackends {
return r.backends
}
// WithRasterBackend 选择当前渲染器的图像、PNG和JPEG后端,不改变SVG、PDF或OFD保存
// 入参: backend 光栅后端,nil禁用图像输出
// 返回: RendererOption 渲染选项
func WithRasterBackend(backend RasterBackend) RendererOption {
return func(r *Renderer) {
if !sameBackend(r.backends.Raster, backend) {
r.backendStates = make(map[any]any)
}
r.backends.Raster = backend
}
}
// CompilePage 编译与后端无关的光栅页面,可交给任意RasterBackend绘制
// 字形、描边、裁剪和底纹沿用文档语义,不交由后端重新排版
// 入参: page 页面内容
// 返回: *RasterPage 只读绘制页面, error 编译错误
func (r *Renderer) CompilePage(page *PageContent) (*RasterPage, error) {
if r.backends.Compiler == nil {
return nil, fmt.Errorf("page compiler: %w", ErrBackendUnavailable)
}
return r.backends.Compiler.CompilePage(r, page)
}
+21 -21
View File
@@ -57,27 +57,6 @@ type readerOptions struct {
provider CryptoProvider provider CryptoProvider
} }
// WithCredentials 添加解密凭据,多次调用可解开多层加密
// 入参: credentials 解密凭据
// 返回: ReaderOption 阅读选项
func WithCredentials(credentials Credentials) ReaderOption {
credentials.Password = bytes.Clone(credentials.Password)
credentials.Certificate = bytes.Clone(credentials.Certificate)
return func(options *readerOptions) {
copy := credentials
copy.Password = bytes.Clone(credentials.Password)
copy.Certificate = bytes.Clone(credentials.Certificate)
options.credentials = append(options.credentials, copy)
}
}
// WithCryptoProvider 设置文档解密的密码提供者
// 入参: provider 密码提供者,nil使用内置实现
// 返回: ReaderOption 阅读选项
func WithCryptoProvider(provider CryptoProvider) ReaderOption {
return func(options *readerOptions) { options.provider = provider }
}
// EncryptionRecipient 证书加密接收者,Certificate为SM2公钥证书的DER编码 // EncryptionRecipient 证书加密接收者,Certificate为SM2公钥证书的DER编码
type EncryptionRecipient struct { type EncryptionRecipient struct {
UserName string UserName string
@@ -110,6 +89,27 @@ type encryptionState struct {
options *EncryptionOptions options *EncryptionOptions
} }
// WithCredentials 添加解密凭据,多次调用可解开多层加密
// 入参: credentials 解密凭据
// 返回: ReaderOption 阅读选项
func WithCredentials(credentials Credentials) ReaderOption {
credentials.Password = bytes.Clone(credentials.Password)
credentials.Certificate = bytes.Clone(credentials.Certificate)
return func(options *readerOptions) {
copy := credentials
copy.Password = bytes.Clone(credentials.Password)
copy.Certificate = bytes.Clone(credentials.Certificate)
options.credentials = append(options.credentials, copy)
}
}
// WithCryptoProvider 设置文档解密的密码提供者
// 入参: provider 密码提供者,nil使用内置实现
// 返回: ReaderOption 阅读选项
func WithCryptoProvider(provider CryptoProvider) ReaderOption {
return func(options *readerOptions) { options.provider = provider }
}
// Encryption 获取文档加密状态,不暴露口令或私钥 // Encryption 获取文档加密状态,不暴露口令或私钥
// 返回: EncryptionInfo 加密状态 // 返回: EncryptionInfo 加密状态
func (r *Reader) Encryption() EncryptionInfo { func (r *Reader) Encryption() EncryptionInfo {
+17 -17
View File
@@ -88,6 +88,23 @@ type encryptionEntry struct {
Seed string `xml:"DecryptSeedLoc,attr,omitempty"` Seed string `xml:"DecryptSeedLoc,attr,omitempty"`
} }
// EncryptPackage 对完整OFD包进行GM/T0099加密,保留全部条目的原始内容
// 先签后加密可保留对明文的验签能力;出错不返回部分密文,已有加密包须先解密
// 入参: data 明文OFD包, options 加密策略
// 返回: []byte 加密OFD包, error 错误信息
func EncryptPackage(data []byte, options EncryptionOptions) ([]byte, error) {
r, err := NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, err
}
defer r.Close()
parts, err := r.packageData()
if err != nil {
return nil, err
}
return encryptPackageParts(parts, options, nil)
}
// open 初始化包索引并在解析OFD根节点前解密 // open 初始化包索引并在解析OFD根节点前解密
// 入参: options 阅读选项 // 入参: options 阅读选项
// 返回: error 错误信息 // 返回: error 错误信息
@@ -406,23 +423,6 @@ func inheritedEncryptionOptions(seed encryptionSeed, credential Credentials, pro
return options return options
} }
// EncryptPackage 对完整OFD包进行GM/T0099加密,保留全部条目的原始内容
// 先签后加密可保留对明文的验签能力;出错不返回部分密文,已有加密包须先解密
// 入参: data 明文OFD包, options 加密策略
// 返回: []byte 加密OFD包, error 错误信息
func EncryptPackage(data []byte, options EncryptionOptions) ([]byte, error) {
r, err := NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, err
}
defer r.Close()
parts, err := r.packageData()
if err != nil {
return nil, err
}
return encryptPackageParts(parts, options, nil)
}
// encryptPackageParts 加密所有原条目,再生成标准入口、密钥描述与映射 // encryptPackageParts 加密所有原条目,再生成标准入口、密钥描述与映射
// 入参: parts 明文条目, options 加密策略, progress 取消与进度检查 // 入参: parts 明文条目, options 加密策略, progress 取消与进度检查
// 返回: []byte 加密包, error 错误信息 // 返回: []byte 加密包, error 错误信息
+29 -29
View File
@@ -190,6 +190,12 @@ type ValidPeriod struct {
EndDate string `xml:"EndDate,attr"` EndDate string `xml:"EndDate,attr"`
} }
// print 打印权限节点
type print struct {
Printable *bool `xml:"Printable,attr"`
Copies *int `xml:"Copies,attr"`
}
// OutlineInfos 获取目录层级和目标页码 // OutlineInfos 获取目录层级和目标页码
// 返回: []OutlineInfo 目录节点信息 // 返回: []OutlineInfo 目录节点信息
func (doc *Document) OutlineInfos() []OutlineInfo { func (doc *Document) OutlineInfos() []OutlineInfo {
@@ -207,31 +213,6 @@ func (doc *Document) OutlineInfos() []OutlineInfo {
return outlineInfos(doc.Outlines.OutlineElem, bookmarks, pages) return outlineInfos(doc.Outlines.OutlineElem, bookmarks, pages)
} }
// outlineInfos 转换目录节点信息
// 入参: outlines 大纲节点, bookmarks 书签, pages 页码索引
// 返回: []OutlineInfo 目录节点信息
func outlineInfos(outlines []OutlineElem, bookmarks map[string]Dest, pages map[string]int) []OutlineInfo {
infos := make([]OutlineInfo, 0, len(outlines))
for _, outline := range outlines {
info := OutlineInfo{Title: outline.Title, Expanded: outline.Expanded}
for _, action := range outline.Actions {
if action.Goto != nil {
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
info.Page = pages[dest.PageID]
if info.Page != 0 {
break
}
}
}
}
if len(outline.OutlineElem) > 0 {
info.Children = outlineInfos(outline.OutlineElem, bookmarks, pages)
}
infos = append(infos, info)
}
return infos
}
// UnmarshalXML 解析文档并应用权限默认值 // UnmarshalXML 解析文档并应用权限默认值
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
@@ -327,10 +308,29 @@ func (p *Permissions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
return nil return nil
} }
// print 打印权限节点 // outlineInfos 转换目录节点信息
type print struct { // 入参: outlines 大纲节点, bookmarks 书签, pages 页码索引
Printable *bool `xml:"Printable,attr"` // 返回: []OutlineInfo 目录节点信息
Copies *int `xml:"Copies,attr"` func outlineInfos(outlines []OutlineElem, bookmarks map[string]Dest, pages map[string]int) []OutlineInfo {
infos := make([]OutlineInfo, 0, len(outlines))
for _, outline := range outlines {
info := OutlineInfo{Title: outline.Title, Expanded: outline.Expanded}
for _, action := range outline.Actions {
if action.Goto != nil {
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
info.Page = pages[dest.PageID]
if info.Page != 0 {
break
}
}
}
}
if len(outline.OutlineElem) > 0 {
info.Children = outlineInfos(outline.OutlineElem, bookmarks, pages)
}
infos = append(infos, info)
}
return infos
} }
// defaultPermissions 获取默认文档权限 // defaultPermissions 获取默认文档权限
+191 -191
View File
@@ -66,6 +66,28 @@ type Editor struct {
encryption *encryptionState encryption *encryptionState
} }
// editorResource 文档内嵌资源
type editorResource struct {
name string
data []byte
font *Font
image *MultiMedia
space *ColorSpace
composite string
references []string
subset *editorFontSubset
states map[string]editorCompositeState
draw *DrawParam
drawSource string
}
// editorResourceKey 资源内容和字体集合索引
type editorResourceKey struct {
checksum [32]byte
index int
kind string
}
// SetFontDirs 设置编辑与几何度量使用的外部字体目录,不嵌入或替换文档字体 // SetFontDirs 设置编辑与几何度量使用的外部字体目录,不嵌入或替换文档字体
// 入参: dirs 字体目录,空参数清除目录配置 // 入参: dirs 字体目录,空参数清除目录配置
func (e *Editor) SetFontDirs(dirs ...string) { func (e *Editor) SetFontDirs(dirs ...string) {
@@ -117,44 +139,6 @@ func (e *Editor) Backends() RenderBackends {
return e.backends return e.backends
} }
// newRenderer 使用编辑器字体来源和完整后端配置创建资源度量器
// 入参: reader 当前文档快照
// 返回: *Renderer 度量器
func (e *Editor) newRenderer(reader *Reader) *Renderer {
return NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends))
}
// editorResource 文档内嵌资源
type editorResource struct {
name string
data []byte
font *Font
image *MultiMedia
space *ColorSpace
composite string
references []string
subset *editorFontSubset
states map[string]editorCompositeState
draw *DrawParam
drawSource string
}
// definition 获取独立定义资源的标识
// 返回: string 复合资源或绘制参数标识
func (r editorResource) definition() string {
if r.draw != nil {
return r.draw.ID
}
return r.composite
}
// editorResourceKey 资源内容和字体集合索引
type editorResourceKey struct {
checksum [32]byte
index int
kind string
}
// NewEditor 创建空白文档,保存前至少添加一页 // NewEditor 创建空白文档,保存前至少添加一页
// 返回: *Editor 文档编辑器 // 返回: *Editor 文档编辑器
func NewEditor() *Editor { func NewEditor() *Editor {
@@ -226,21 +210,6 @@ func (e *Editor) Page(index int) (*PageContent, error) {
return &result, nil return &result, nil
} }
// page 按索引获取内部页面
// 入参: index 页面索引
// 返回: *PageContent 页面内容, error 错误信息
func (e *Editor) page(index int) (*PageContent, error) {
if index < 0 || index >= len(e.pages) {
return nil, fmt.Errorf("page index %d out of range", index)
}
if e.source != nil {
if err := e.loadSourcePage(index); err != nil {
return nil, err
}
}
return &e.pages[index], nil
}
// AddPage 添加页面及默认正文图层 // AddPage 添加页面及默认正文图层
// 入参: width 页面宽度, height 页面高度 // 入参: width 页面宽度, height 页面高度
// 返回: int 页面索引, error 错误信息 // 返回: int 页面索引, error 错误信息
@@ -363,23 +332,6 @@ func (e *Editor) DeletePages(indexes []int) error {
return nil return nil
} }
// validatePageIndexes 校验页面索引范围和重复值
// 入参: indexes 页面索引
// 返回: error 错误信息
func (e *Editor) validatePageIndexes(indexes []int) error {
seen := make(map[int]bool, len(indexes))
for _, index := range indexes {
if index < 0 || index >= len(e.pages) {
return fmt.Errorf("page index %d out of range", index)
}
if seen[index] {
return fmt.Errorf("duplicate page index %d", index)
}
seen[index] = true
}
return nil
}
// MovePage 移动页面,保留页面及对象标识 // MovePage 移动页面,保留页面及对象标识
// 入参: from 原页面索引, to 移动后的页面索引 // 入参: from 原页面索引, to 移动后的页面索引
// 返回: error 错误信息 // 返回: error 错误信息
@@ -445,15 +397,6 @@ func (e *Editor) MovePages(indexes []int, to int) error {
return nil return nil
} }
// reorderPages 按原页面索引序列调整页面顺序
// 入参: order 页面索引序列
func (e *Editor) reorderPages(order []int) {
pages := slices.Clone(e.pages)
for index, previous := range order {
e.pages[index] = pages[previous]
}
}
// ResizePage 调整页面尺寸,不缩放或移动页面内的对象 // ResizePage 调整页面尺寸,不缩放或移动页面内的对象
// 入参: index 页面索引, width 页面宽度, height 页面高度 // 入参: index 页面索引, width 页面宽度, height 页面高度
// 返回: error 错误信息 // 返回: error 错误信息
@@ -503,13 +446,6 @@ func (e *Editor) ResizePages(indexes []int, width, height float64) error {
return nil return nil
} }
// nextID 分配文档内唯一标识
// 返回: string 对象标识
func (e *Editor) nextID() string {
e.maxID++
return strconv.Itoa(e.maxID)
}
// AddImage 注册PNG、JPEG或JBIG2图片,重复资源复用标识,引用后写入文档 // AddImage 注册PNG、JPEG或JBIG2图片,重复资源复用标识,引用后写入文档
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码 // 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
// 入参: data 图片数据 // 入参: data 图片数据
@@ -552,21 +488,6 @@ func (e *Editor) AddImage(data []byte) (string, error) {
return id, nil return id, nil
} }
// editorBinaryImage 尝试纯黑白无损编码,不二值化,不改变透明度,无体积收益时保留原图
// 入参: data PNG图片数据
// 返回: []byte 更小的JBIG2数据,不适用时为nil
func editorBinaryImage(data []byte) []byte {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil
}
var output bytes.Buffer
if err := jbig2.Encode(&output, img, &jbig2.Options{MaxPageBytes: uint64(len(data))}); err != nil || output.Len() >= len(data) {
return nil
}
return output.Bytes()
}
// AddObject 按添加顺序放入正文图层,支持文字、路径和图片,自动分配对象ID // AddObject 按添加顺序放入正文图层,支持文字、路径和图片,自动分配对象ID
// 支持基本颜色、直接资源引用及路径裁剪,不支持动作、渐变及复合图元 // 支持基本颜色、直接资源引用及路径裁剪,不支持动作、渐变及复合图元
// 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据 // 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据
@@ -658,6 +579,175 @@ func (e *Editor) MoveObject(page int, id string, to int) error {
return nil return nil
} }
// AddText 添加文字,按显式换行和嵌入字体度量定位,不自动折行或进行复杂文字塑形
// 入参: page 页面索引, box 文字边界, value 原文, fontID 字体资源标识, size 字号
// 返回: string 对象标识, error 错误信息
func (e *Editor) AddText(page int, box Box, value, fontID string, size float64) (string, error) {
object := GraphicObject{Type: "TextObject", TextObject: TextObject{
Boundary: fmt.Sprintf("%s %s %s %s", ofdNumber(box.X), ofdNumber(box.Y), ofdNumber(box.W), ofdNumber(box.H)),
Font: fontID, Size: size,
}}
if err := e.LayoutText(&object.TextObject, value, TextLayout{}); err != nil {
return "", err
}
return e.AddObject(page, object)
}
// UpdateText 按现有段落选项重排横向文字,保留对象标识、顺序、边界及绘制属性
// 自定义文字定位使用UpdateObject,字体塑形沿用既有段落选项
// 入参: page 页面索引, id 文字对象标识, value 原文, fontID 字体资源标识, size 字号
// 返回: error 错误信息
func (e *Editor) UpdateText(page int, id, value, fontID string, size float64) error {
layer, index, err := e.findObject(page, id)
if err != nil {
return err
}
object := layer.Objects[index]
if object.Type != "TextObject" {
return fmt.Errorf("object %q is not text", id)
}
object.TextObject.Font, object.TextObject.Size = fontID, size
_, layout := object.TextObject.TextLayout()
if err := e.LayoutText(&object.TextObject, value, layout); err != nil {
return err
}
return e.UpdateObject(page, id, object)
}
// TransformObject 以页面原点等比缩放后平移对象,文字同步缩放字号和字距,保留原有排版
// 带边框图片缩放时转为保留原图片与边框的复合对象,标识不变
// 入参: page 页面索引, id 对象标识, dx 横向位移, dy 纵向位移, scale 正缩放比例
// 返回: error 错误信息
func (e *Editor) TransformObject(page int, id string, dx, dy, scale float64) error {
return e.TransformObjects(page, []string{id}, dx, dy, scale)
}
// ReplaceImage 替换图片资源,保留对象标识和绘制顺序,一次撤销恢复资源及布局
// 入参: page 页面索引, id 图片对象标识, data PNG或JPEG数据, fit 为contain、cover或空,空值保留原变换和裁剪
// 返回: error 错误信息
func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) error {
object, err := e.Object(page, id)
if err != nil {
return err
}
if object.Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", id)
}
object.ImageObject.ResourceID, err = e.AddImage(data)
if err != nil {
return err
}
if fit != "" {
if err := e.fitImage(&object.ImageObject, fit); err != nil {
return err
}
}
return e.UpdateObject(page, id, object)
}
// AlignObject 将对象对齐页面,文字采用实际字形范围,路径和图片采用对象边界
// 入参: page 页面索引, id 对象标识, alignment 为left、center、right、top、middle或bottom
// 返回: error 错误信息
func (e *Editor) AlignObject(page int, id, alignment string) error {
return e.AlignObjects(page, []string{id}, alignment)
}
// LayoutText 使用编辑器字体后端重排文字,不修改文档和历史
// 入参: obj 文字对象, value 原文, options 段落排版选项
// 返回: error 字体或排版错误
func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) error {
metrics, err := e.editorFont(obj.Font)
if err != nil {
return err
}
if err := LayoutText(obj, value, options, metrics); err != nil {
return err
}
if obj.layout == nil && e.objectOrigin(obj.ID) != nil {
obj.layout = &textLayout{value: obj.Text(), options: options}
}
return nil
}
// newRenderer 使用编辑器字体来源和完整后端配置创建资源度量器
// 入参: reader 当前文档快照
// 返回: *Renderer 度量器
func (e *Editor) newRenderer(reader *Reader) *Renderer {
return NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends))
}
// definition 获取独立定义资源的标识
// 返回: string 复合资源或绘制参数标识
func (r editorResource) definition() string {
if r.draw != nil {
return r.draw.ID
}
return r.composite
}
// page 按索引获取内部页面
// 入参: index 页面索引
// 返回: *PageContent 页面内容, error 错误信息
func (e *Editor) page(index int) (*PageContent, error) {
if index < 0 || index >= len(e.pages) {
return nil, fmt.Errorf("page index %d out of range", index)
}
if e.source != nil {
if err := e.loadSourcePage(index); err != nil {
return nil, err
}
}
return &e.pages[index], nil
}
// validatePageIndexes 校验页面索引范围和重复值
// 入参: indexes 页面索引
// 返回: error 错误信息
func (e *Editor) validatePageIndexes(indexes []int) error {
seen := make(map[int]bool, len(indexes))
for _, index := range indexes {
if index < 0 || index >= len(e.pages) {
return fmt.Errorf("page index %d out of range", index)
}
if seen[index] {
return fmt.Errorf("duplicate page index %d", index)
}
seen[index] = true
}
return nil
}
// reorderPages 按原页面索引序列调整页面顺序
// 入参: order 页面索引序列
func (e *Editor) reorderPages(order []int) {
pages := slices.Clone(e.pages)
for index, previous := range order {
e.pages[index] = pages[previous]
}
}
// nextID 分配文档内唯一标识
// 返回: string 对象标识
func (e *Editor) nextID() string {
e.maxID++
return strconv.Itoa(e.maxID)
}
// editorBinaryImage 尝试纯黑白无损编码,不二值化,不改变透明度,无体积收益时保留原图
// 入参: data PNG图片数据
// 返回: []byte 更小的JBIG2数据,不适用时为nil
func editorBinaryImage(data []byte) []byte {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil
}
var output bytes.Buffer
if err := jbig2.Encode(&output, img, &jbig2.Options{MaxPageBytes: uint64(len(data))}); err != nil || output.Len() >= len(data) {
return nil
}
return output.Bytes()
}
// findObject 查找页面各图层中的对象 // findObject 查找页面各图层中的对象
// 入参: page 页面索引, id 对象标识 // 入参: page 页面索引, id 对象标识
// 返回: *Layer 所属图层, int 对象索引, error 错误信息 // 返回: *Layer 所属图层, int 对象索引, error 错误信息
@@ -835,49 +925,6 @@ func cloneEditorObject(object GraphicObject) (GraphicObject, error) {
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type) return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
} }
// AddText 添加文字,按显式换行和嵌入字体度量定位,不自动折行或进行复杂文字塑形
// 入参: page 页面索引, box 文字边界, value 原文, fontID 字体资源标识, size 字号
// 返回: string 对象标识, error 错误信息
func (e *Editor) AddText(page int, box Box, value, fontID string, size float64) (string, error) {
object := GraphicObject{Type: "TextObject", TextObject: TextObject{
Boundary: fmt.Sprintf("%s %s %s %s", ofdNumber(box.X), ofdNumber(box.Y), ofdNumber(box.W), ofdNumber(box.H)),
Font: fontID, Size: size,
}}
if err := e.LayoutText(&object.TextObject, value, TextLayout{}); err != nil {
return "", err
}
return e.AddObject(page, object)
}
// UpdateText 按现有段落选项重排横向文字,保留对象标识、顺序、边界及绘制属性
// 自定义文字定位使用UpdateObject,字体塑形沿用既有段落选项
// 入参: page 页面索引, id 文字对象标识, value 原文, fontID 字体资源标识, size 字号
// 返回: error 错误信息
func (e *Editor) UpdateText(page int, id, value, fontID string, size float64) error {
layer, index, err := e.findObject(page, id)
if err != nil {
return err
}
object := layer.Objects[index]
if object.Type != "TextObject" {
return fmt.Errorf("object %q is not text", id)
}
object.TextObject.Font, object.TextObject.Size = fontID, size
_, layout := object.TextObject.TextLayout()
if err := e.LayoutText(&object.TextObject, value, layout); err != nil {
return err
}
return e.UpdateObject(page, id, object)
}
// TransformObject 以页面原点等比缩放后平移对象,文字同步缩放字号和字距,保留原有排版
// 带边框图片缩放时转为保留原图片与边框的复合对象,标识不变
// 入参: page 页面索引, id 对象标识, dx 横向位移, dy 纵向位移, scale 正缩放比例
// 返回: error 错误信息
func (e *Editor) TransformObject(page int, id string, dx, dy, scale float64) error {
return e.TransformObjects(page, []string{id}, dx, dy, scale)
}
// transformEditorObject 变换独立对象副本,保留绘制属性和编辑中的段落信息 // transformEditorObject 变换独立对象副本,保留绘制属性和编辑中的段落信息
// 入参: object 对象副本, dx 横向位移, dy 纵向位移, scale 正缩放比例 // 入参: object 对象副本, dx 横向位移, dy 纵向位移, scale 正缩放比例
// 返回: GraphicObject 变换后的对象, error 错误信息 // 返回: GraphicObject 变换后的对象, error 错误信息
@@ -940,36 +987,6 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
return object, nil return object, nil
} }
// ReplaceImage 替换图片资源,保留对象标识和绘制顺序,一次撤销恢复资源及布局
// 入参: page 页面索引, id 图片对象标识, data PNG或JPEG数据, fit 为contain、cover或空,空值保留原变换和裁剪
// 返回: error 错误信息
func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) error {
object, err := e.Object(page, id)
if err != nil {
return err
}
if object.Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", id)
}
object.ImageObject.ResourceID, err = e.AddImage(data)
if err != nil {
return err
}
if fit != "" {
if err := e.fitImage(&object.ImageObject, fit); err != nil {
return err
}
}
return e.UpdateObject(page, id, object)
}
// AlignObject 将对象对齐页面,文字采用实际字形范围,路径和图片采用对象边界
// 入参: page 页面索引, id 对象标识, alignment 为left、center、right、top、middle或bottom
// 返回: error 错误信息
func (e *Editor) AlignObject(page int, id, alignment string) error {
return e.AlignObjects(page, []string{id}, alignment)
}
// scaleTextNumbers 缩放已校验的文字定位数值,保留g重复编码 // scaleTextNumbers 缩放已校验的文字定位数值,保留g重复编码
// 入参: value 定位数值, scale 缩放比例 // 入参: value 定位数值, scale 缩放比例
// 返回: string 缩放后的数值 // 返回: string 缩放后的数值
@@ -985,23 +1002,6 @@ func scaleTextNumbers(value string, scale float64) string {
return strings.Join(fields, " ") return strings.Join(fields, " ")
} }
// LayoutText 使用编辑器字体后端重排文字,不修改文档和历史
// 入参: obj 文字对象, value 原文, options 段落排版选项
// 返回: error 字体或排版错误
func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) error {
metrics, err := e.editorFont(obj.Font)
if err != nil {
return err
}
if err := LayoutText(obj, value, options, metrics); err != nil {
return err
}
if obj.layout == nil && e.objectOrigin(obj.ID) != nil {
obj.layout = &textLayout{value: obj.Text(), options: options}
}
return nil
}
// prepareText 校验并补全标准文字定位,避免依赖阅读器的缺省字距补偿 // prepareText 校验并补全标准文字定位,避免依赖阅读器的缺省字距补偿
// 入参: obj 文字对象 // 入参: obj 文字对象
// 返回: error 错误信息 // 返回: error 错误信息
+193 -193
View File
@@ -28,14 +28,18 @@ type editorObjectPosition struct {
index int index int
} }
// compareEditorPosition 比较对象在页面结构中的先后顺序 // ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象
// 入参: a、b 对象位置 type ObjectPosition struct {
// 返回: int 比较结果 Layer string
func compareEditorPosition(a, b editorObjectPosition) int { Container string
if value := cmp.Compare(a.layer, b.layer); value != 0 { Index int
return value Count int
} }
return cmp.Compare(a.index, b.index)
// ObjectInfo 对象的编辑能力及直接容器位置
type ObjectInfo struct {
Capabilities ObjectCapabilities
Position ObjectPosition
} }
// Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息 // Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息
@@ -134,20 +138,6 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
return result, nil return result, nil
} }
// ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象
type ObjectPosition struct {
Layer string
Container string
Index int
Count int
}
// ObjectInfo 对象的编辑能力及直接容器位置
type ObjectInfo struct {
Capabilities ObjectCapabilities
Position ObjectPosition
}
// PageObjectInfo 批量获取页面对象能力与位置,不重复扫描同一容器 // PageObjectInfo 批量获取页面对象能力与位置,不重复扫描同一容器
// 每次按当前页面计算,变更和撤销后无需失效旧缓存 // 每次按当前页面计算,变更和撤销后无需失效旧缓存
// 入参: page 页面索引 // 入参: page 页面索引
@@ -211,6 +201,187 @@ func (e *Editor) ObjectPosition(page int, id string) (ObjectPosition, error) {
return position, nil return position, nil
} }
// OrderObjects 调整同一图层或页块内选区的绘制顺序,保留各组选中及未选中对象的相对顺序
// 入参: page 页面索引, ids 对象标识, order 为up、down、top或bottom
// 返回: error 错误信息
func (e *Editor) OrderObjects(page int, ids []string, order string) error {
if !slices.Contains([]string{"up", "down", "top", "bottom"}, order) {
return fmt.Errorf("invalid object order %q", order)
}
_, indexes, err := e.selectedObjects(page, ids)
if err != nil || len(indexes) == 0 {
return err
}
layer := &e.pages[page].Content.Layer[indexes[0].layer]
_, siblings := e.objectOrderIndexes(page, layer, ids[0])
for i, id := range ids {
capability, err := e.ObjectCapabilities(page, id)
if err != nil {
return err
}
if !capability.Order || indexes[i].layer != indexes[0].layer || !slices.Contains(siblings, indexes[i].index) {
return fmt.Errorf("ordering requires editable objects in the same container")
}
}
layers := copyEditorPage(e.pages[page]).Content.Layer
objects := make([]GraphicObject, len(siblings))
for i, index := range siblings {
objects[i] = layer.Objects[index]
}
selected := make([]bool, len(objects))
for _, index := range indexes {
selected[slices.Index(siblings, index.index)] = true
}
orderEditorObjects(objects, selected, order)
for i, index := range siblings {
layers[indexes[0].layer].Objects[index] = objects[i]
}
e.replaceLayers(page, layers)
return nil
}
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序
// 不足三个对象时不修改;允许负间距,无法保持位置顺序的重叠布局返回错误
// 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical
// 返回: error 错误信息
func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
if axis != "horizontal" && axis != "vertical" {
return fmt.Errorf("invalid distribution axis %q", axis)
}
objects, indexes, err := e.selectedObjects(page, ids)
if err != nil || len(objects) < 3 {
return err
}
boxes, err := e.objectBounds(page, objects)
if err != nil {
return err
}
matrices, err := editorDistribution(boxes, indexes, axis)
if err != nil {
return err
}
var updates []GraphicObject
for i, matrix := range matrices {
if matrix == IdentityMatrix {
continue
}
object, err := cloneEditorObject(objects[i])
if err != nil {
return err
}
object, err = e.transformObject(object, matrix.e, matrix.f, 1)
if err != nil {
return err
}
updates = append(updates, object)
}
return e.updateObjects(page, updates, true)
}
// UpdateObjects 原子替换同页对象,按各对象ID定位,全部校验通过后提交一次历史记录
// 入参: page 页面索引, objects 新对象内容,ID不得重复
// 返回: error 错误信息
func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error {
return e.updateObjects(page, objects, false)
}
// TransformObjects 同页对象以页面原点等比缩放后统一平移,保持相对位置,一次撤销恢复全部
// 入参: page 页面索引, ids 对象标识, dx、dy 位移, scale 正缩放比例
// 返回: error 错误信息
func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64) error {
if !finite(dx) || !finite(dy) || !finite(scale) || scale <= 0 {
return fmt.Errorf("transform requires finite offsets and a positive finite scale")
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil {
return err
}
if dx == 0 && dy == 0 && scale == 1 {
return nil
}
return e.transformObjects(page, objects, func(object GraphicObject) (GraphicObject, error) {
if e.originalPage(page) {
if err := validateEditorGeometry(object); err != nil {
return GraphicObject{}, err
}
}
return e.transformObject(object, dx, dy, scale)
})
}
// AlignObjects 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围
// 入参: page 页面索引, ids 对象标识, alignment 为left、center、right、top、middle或bottom
// 返回: error 错误信息
func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
if !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, alignment) {
return fmt.Errorf("invalid alignment %q", alignment)
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil || len(objects) == 0 {
return err
}
boxes, err := e.objectBounds(page, objects)
if err != nil {
return err
}
target, _ := ParseBox(e.pages[page].Area.PhysicalBox)
if len(objects) > 1 {
target = Box{}
for _, box := range boxes {
target = unionTextBox(target, box)
}
}
for i, box := range boxes {
matrix := editorAlignment(box, target, alignment)
objects[i], err = cloneEditorObject(objects[i])
if err != nil {
return err
}
objects[i], err = e.transformObject(objects[i], matrix.e, matrix.f, 1)
if err != nil {
return err
}
}
return e.updateObjects(page, objects, true)
}
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部
// 入参: page 页面索引, ids 对象标识
// 返回: error 错误信息
func (e *Editor) DeleteObjects(page int, ids []string) error {
_, indexes, err := e.selectedObjects(page, ids)
if err != nil || len(indexes) == 0 {
return err
}
for _, id := range ids {
capability, err := e.ObjectCapabilities(page, id)
if err != nil {
return err
}
if !capability.Delete {
return fmt.Errorf("object %q cannot be deleted: %w", id, capability.editError())
}
}
layers := copyEditorPage(e.pages[page]).Content.Layer
slices.SortFunc(indexes, compareEditorPosition)
for _, index := range slices.Backward(indexes) {
layer := &layers[index.layer]
layer.Objects = slices.Delete(layer.Objects, index.index, index.index+1)
}
e.replaceLayers(page, layers)
return nil
}
// compareEditorPosition 比较对象在页面结构中的先后顺序
// 入参: a、b 对象位置
// 返回: int 比较结果
func compareEditorPosition(a, b editorObjectPosition) int {
if value := cmp.Compare(a.layer, b.layer); value != 0 {
return value
}
return cmp.Compare(a.index, b.index)
}
// objectOrderIndexes 获取同一直接容器的对象索引,不跨越图层或页块 // objectOrderIndexes 获取同一直接容器的对象索引,不跨越图层或页块
// 入参: page 页面索引, layer 图层, id 对象标识 // 入参: page 页面索引, layer 图层, id 对象标识
// 返回: *editorXML 原始容器, []int 对象索引 // 返回: *editorXML 原始容器, []int 对象索引
@@ -253,45 +424,6 @@ func (e *Editor) objectOrderParent(nodes map[string]*editorXML, id string) *edit
return nil return nil
} }
// OrderObjects 调整同一图层或页块内选区的绘制顺序,保留各组选中及未选中对象的相对顺序
// 入参: page 页面索引, ids 对象标识, order 为up、down、top或bottom
// 返回: error 错误信息
func (e *Editor) OrderObjects(page int, ids []string, order string) error {
if !slices.Contains([]string{"up", "down", "top", "bottom"}, order) {
return fmt.Errorf("invalid object order %q", order)
}
_, indexes, err := e.selectedObjects(page, ids)
if err != nil || len(indexes) == 0 {
return err
}
layer := &e.pages[page].Content.Layer[indexes[0].layer]
_, siblings := e.objectOrderIndexes(page, layer, ids[0])
for i, id := range ids {
capability, err := e.ObjectCapabilities(page, id)
if err != nil {
return err
}
if !capability.Order || indexes[i].layer != indexes[0].layer || !slices.Contains(siblings, indexes[i].index) {
return fmt.Errorf("ordering requires editable objects in the same container")
}
}
layers := copyEditorPage(e.pages[page]).Content.Layer
objects := make([]GraphicObject, len(siblings))
for i, index := range siblings {
objects[i] = layer.Objects[index]
}
selected := make([]bool, len(objects))
for _, index := range indexes {
selected[slices.Index(siblings, index.index)] = true
}
orderEditorObjects(objects, selected, order)
for i, index := range siblings {
layers[indexes[0].layer].Objects[index] = objects[i]
}
e.replaceLayers(page, layers)
return nil
}
// orderEditorObjects 调整选区层级,保持选中及未选中对象各自的相对顺序 // orderEditorObjects 调整选区层级,保持选中及未选中对象各自的相对顺序
// 入参: objects 同容器对象, selected 选中状态, order 排序方向 // 入参: objects 同容器对象, selected 选中状态, order 排序方向
func orderEditorObjects[T any](objects []T, selected []bool, order string) { func orderEditorObjects[T any](objects []T, selected []bool, order string) {
@@ -323,44 +455,6 @@ func orderEditorObjects[T any](objects []T, selected []bool, order string) {
} }
} }
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序
// 不足三个对象时不修改;允许负间距,无法保持位置顺序的重叠布局返回错误
// 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical
// 返回: error 错误信息
func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
if axis != "horizontal" && axis != "vertical" {
return fmt.Errorf("invalid distribution axis %q", axis)
}
objects, indexes, err := e.selectedObjects(page, ids)
if err != nil || len(objects) < 3 {
return err
}
boxes, err := e.objectBounds(page, objects)
if err != nil {
return err
}
matrices, err := editorDistribution(boxes, indexes, axis)
if err != nil {
return err
}
var updates []GraphicObject
for i, matrix := range matrices {
if matrix == IdentityMatrix {
continue
}
object, err := cloneEditorObject(objects[i])
if err != nil {
return err
}
object, err = e.transformObject(object, matrix.e, matrix.f, 1)
if err != nil {
return err
}
updates = append(updates, object)
}
return e.updateObjects(page, updates, true)
}
// editorDistribution 计算等间距平移,位置相同时按原绘制顺序排序 // editorDistribution 计算等间距平移,位置相同时按原绘制顺序排序
// 入参: boxes 可见范围, indexes 绘制位置, axis 分布方向 // 入参: boxes 可见范围, indexes 绘制位置, axis 分布方向
// 返回: []Matrix 页面平移, error 错误信息 // 返回: []Matrix 页面平移, error 错误信息
@@ -434,13 +528,6 @@ func (e *Editor) replaceLayers(page int, layers []Layer) {
} }
} }
// UpdateObjects 原子替换同页对象,按各对象ID定位,全部校验通过后提交一次历史记录
// 入参: page 页面索引, objects 新对象内容,ID不得重复
// 返回: error 错误信息
func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error {
return e.updateObjects(page, objects, false)
}
// updateObjects 校验内容更新或几何与外观变换,原子提交跨图层选区 // updateObjects 校验内容更新或几何与外观变换,原子提交跨图层选区
// 入参: page 页面索引, objects 新对象, preserveContent 是否保留原内容 // 入参: page 页面索引, objects 新对象, preserveContent 是否保留原内容
// 返回: error 错误信息 // 返回: error 错误信息
@@ -545,66 +632,6 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve
return nil return nil
} }
// TransformObjects 同页对象以页面原点等比缩放后统一平移,保持相对位置,一次撤销恢复全部
// 入参: page 页面索引, ids 对象标识, dx、dy 位移, scale 正缩放比例
// 返回: error 错误信息
func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64) error {
if !finite(dx) || !finite(dy) || !finite(scale) || scale <= 0 {
return fmt.Errorf("transform requires finite offsets and a positive finite scale")
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil {
return err
}
if dx == 0 && dy == 0 && scale == 1 {
return nil
}
return e.transformObjects(page, objects, func(object GraphicObject) (GraphicObject, error) {
if e.originalPage(page) {
if err := validateEditorGeometry(object); err != nil {
return GraphicObject{}, err
}
}
return e.transformObject(object, dx, dy, scale)
})
}
// AlignObjects 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围
// 入参: page 页面索引, ids 对象标识, alignment 为left、center、right、top、middle或bottom
// 返回: error 错误信息
func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
if !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, alignment) {
return fmt.Errorf("invalid alignment %q", alignment)
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil || len(objects) == 0 {
return err
}
boxes, err := e.objectBounds(page, objects)
if err != nil {
return err
}
target, _ := ParseBox(e.pages[page].Area.PhysicalBox)
if len(objects) > 1 {
target = Box{}
for _, box := range boxes {
target = unionTextBox(target, box)
}
}
for i, box := range boxes {
matrix := editorAlignment(box, target, alignment)
objects[i], err = cloneEditorObject(objects[i])
if err != nil {
return err
}
objects[i], err = e.transformObject(objects[i], matrix.e, matrix.f, 1)
if err != nil {
return err
}
}
return e.updateObjects(page, objects, true)
}
// editorAlignment 计算对象范围到目标边缘或中心的平移 // editorAlignment 计算对象范围到目标边缘或中心的平移
// 入参: box 对象范围, target 目标范围, alignment 对齐方式 // 入参: box 对象范围, target 目标范围, alignment 对齐方式
// 返回: Matrix 页面平移 // 返回: Matrix 页面平移
@@ -633,33 +660,6 @@ func editorAlignment(box, target Box, alignment string) Matrix {
return matrix return matrix
} }
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部
// 入参: page 页面索引, ids 对象标识
// 返回: error 错误信息
func (e *Editor) DeleteObjects(page int, ids []string) error {
_, indexes, err := e.selectedObjects(page, ids)
if err != nil || len(indexes) == 0 {
return err
}
for _, id := range ids {
capability, err := e.ObjectCapabilities(page, id)
if err != nil {
return err
}
if !capability.Delete {
return fmt.Errorf("object %q cannot be deleted: %w", id, capability.editError())
}
}
layers := copyEditorPage(e.pages[page]).Content.Layer
slices.SortFunc(indexes, compareEditorPosition)
for _, index := range slices.Backward(indexes) {
layer := &layers[index.layer]
layer.Objects = slices.Delete(layer.Objects, index.index, index.index+1)
}
e.replaceLayers(page, layers)
return nil
}
// selectedObjects 校验同页选择,返回内部只读对象与对应索引 // selectedObjects 校验同页选择,返回内部只读对象与对应索引
// 入参: page 页面索引, ids 对象标识,不得重复 // 入参: page 页面索引, ids 对象标识,不得重复
// 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息 // 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息
+137 -137
View File
@@ -85,67 +85,6 @@ type editorCompositeCrop struct {
exists bool exists bool
} }
// setStates 独立保存当前XML子树的会话信息,共享不可变记录
// 入参: states 来源记录
func (n *editorCompositeNode) setStates(states map[string]editorCompositeState) {
n.states = make(map[string]editorCompositeState)
var visit func(*editorXML)
visit = func(node *editorXML) {
if state, ok := states[node.attr("ID")]; ok {
n.states[node.attr("ID")] = state
}
for _, child := range node.children {
visit(child)
}
}
visit(n.node)
n.object.TextObject.layout = n.states[editorObjectID(n.object)].layout
}
// setState 替换当前对象的会话记录并计入本次事务
// 入参: state 新记录
func (n *editorCompositeNode) setState(state editorCompositeState) {
id := editorObjectID(n.object)
if reflect.DeepEqual(n.states[id], state) {
return
}
if n.states == nil {
n.states = make(map[string]editorCompositeState)
}
if state == (editorCompositeState{}) {
delete(n.states, id)
} else {
n.states[id] = state
}
n.object.TextObject.layout = state.layout
n.changed = true
}
// remapCompositeStates 随XML标识重映射会话记录,仅保留当前子树的对象
// 入参: states 原记录, ids 标识映射
// 返回: map[string]editorCompositeState 新记录
func remapCompositeStates(states map[string]editorCompositeState, ids map[string]string) map[string]editorCompositeState {
var result map[string]editorCompositeState
for id, next := range ids {
if state, ok := states[id]; ok {
if result == nil {
result = make(map[string]editorCompositeState)
}
result[next] = state
}
}
return result
}
// removeCompositeStates 清除子树旧记录,再由写入结果合并新记录
// 入参: states 目标记录, node 子树根节点
func removeCompositeStates(states map[string]editorCompositeState, node *editorXML) {
delete(states, node.attr("ID"))
for _, child := range node.children {
removeCompositeStates(states, child)
}
}
// CompositeObjects 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级 // CompositeObjects 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级
// 入参: page 页面索引, path 复合对象路径 // 入参: page 页面索引, path 复合对象路径
// 返回: []CompositeMember 按绘制顺序排列的成员, error 错误信息 // 返回: []CompositeMember 按绘制顺序排列的成员, error 错误信息
@@ -285,6 +224,143 @@ func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int
}) })
} }
// StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽
// 不重排文字,不改写继承参数;显式实线覆盖原虚线继承
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米
// 返回: error 错误信息
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) error {
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil || style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
for i, node := range nodes {
capability := members[i].Capabilities
if !capability.Transform || paint && !capability.Paint {
return fmt.Errorf("composite member %d cannot accept this style: %w", indexes[i], capability.editError())
}
local := style
switch members[i].Object.Type {
case "TextObject":
local.FillColor = editorStyleColor(style.FillColor, members[i].Object.TextObject.FillColor)
case "PathObject":
p := members[i].Object.PathObject
local.FillColor = editorStyleColor(style.FillColor, p.FillColor)
local.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(p.StrokeColor)))
if style.LineWidth != nil {
width := *style.LineWidth / members[i].StrokeScale
local.LineWidth = &width
}
}
object, err := e.styleObject(node.object, local)
if err != nil {
return err
}
if err := node.update(object); err != nil {
return err
}
}
return nil
})
}
// AlignCompositeObjects 将内部多选成员对齐至选区边界,单选时对齐页面
// 入参: page 页面索引, path 父路径, indexes 成员序号, alignment 为left、center、right、top、middle或bottom
// 返回: error 错误信息
func (e *Editor) AlignCompositeObjects(page int, path ObjectPath, indexes []int, alignment string) error {
if !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, alignment) {
return fmt.Errorf("invalid alignment %q", alignment)
}
return e.arrangeCompositeObjects(page, path, indexes, func(boxes []Box) ([]Matrix, error) {
target, _ := ParseBox(e.pages[page].Area.PhysicalBox)
if len(boxes) > 1 {
target = Box{}
for _, box := range boxes {
target = unionTextBox(target, box)
}
}
matrices := make([]Matrix, len(boxes))
for i, box := range boxes {
matrices[i] = editorAlignment(box, target, alignment)
}
return matrices, nil
})
}
// DistributeCompositeObjects 按可见范围等距分布内部成员,固定两端并保留绘制顺序
// 入参: page 页面索引, path 父路径, indexes 成员序号, axis 为horizontal或vertical
// 返回: error 错误信息
func (e *Editor) DistributeCompositeObjects(page int, path ObjectPath, indexes []int, axis string) error {
if axis != "horizontal" && axis != "vertical" {
return fmt.Errorf("invalid distribution axis %q", axis)
}
return e.arrangeCompositeObjects(page, path, indexes, func(boxes []Box) ([]Matrix, error) {
positions := make([]editorObjectPosition, len(indexes))
for i, index := range indexes {
positions[i].index = index
}
return editorDistribution(boxes, positions, axis)
})
}
// setStates 独立保存当前XML子树的会话信息,共享不可变记录
// 入参: states 来源记录
func (n *editorCompositeNode) setStates(states map[string]editorCompositeState) {
n.states = make(map[string]editorCompositeState)
var visit func(*editorXML)
visit = func(node *editorXML) {
if state, ok := states[node.attr("ID")]; ok {
n.states[node.attr("ID")] = state
}
for _, child := range node.children {
visit(child)
}
}
visit(n.node)
n.object.TextObject.layout = n.states[editorObjectID(n.object)].layout
}
// setState 替换当前对象的会话记录并计入本次事务
// 入参: state 新记录
func (n *editorCompositeNode) setState(state editorCompositeState) {
id := editorObjectID(n.object)
if reflect.DeepEqual(n.states[id], state) {
return
}
if n.states == nil {
n.states = make(map[string]editorCompositeState)
}
if state == (editorCompositeState{}) {
delete(n.states, id)
} else {
n.states[id] = state
}
n.object.TextObject.layout = state.layout
n.changed = true
}
// remapCompositeStates 随XML标识重映射会话记录,仅保留当前子树的对象
// 入参: states 原记录, ids 标识映射
// 返回: map[string]editorCompositeState 新记录
func remapCompositeStates(states map[string]editorCompositeState, ids map[string]string) map[string]editorCompositeState {
var result map[string]editorCompositeState
for id, next := range ids {
if state, ok := states[id]; ok {
if result == nil {
result = make(map[string]editorCompositeState)
}
result[next] = state
}
}
return result
}
// removeCompositeStates 清除子树旧记录,再由写入结果合并新记录
// 入参: states 目标记录, node 子树根节点
func removeCompositeStates(states map[string]editorCompositeState, node *editorXML) {
delete(states, node.attr("ID"))
for _, child := range node.children {
removeCompositeStates(states, child)
}
}
// compositeOrientation 将旋转或镜像中心移至选区中心 // compositeOrientation 将旋转或镜像中心移至选区中心
// 入参: box 选区, matrix 方向矩阵 // 入参: box 选区, matrix 方向矩阵
// 返回: Matrix 页面变换 // 返回: Matrix 页面变换
@@ -616,82 +692,6 @@ func (e *Editor) compositeMemberStyle(node *editorCompositeNode) (GraphicObject,
return object, err return object, err
} }
// StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽
// 不重排文字,不改写继承参数;显式实线覆盖原虚线继承
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米
// 返回: error 错误信息
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) error {
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil || style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
for i, node := range nodes {
capability := members[i].Capabilities
if !capability.Transform || paint && !capability.Paint {
return fmt.Errorf("composite member %d cannot accept this style: %w", indexes[i], capability.editError())
}
local := style
switch members[i].Object.Type {
case "TextObject":
local.FillColor = editorStyleColor(style.FillColor, members[i].Object.TextObject.FillColor)
case "PathObject":
p := members[i].Object.PathObject
local.FillColor = editorStyleColor(style.FillColor, p.FillColor)
local.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(p.StrokeColor)))
if style.LineWidth != nil {
width := *style.LineWidth / members[i].StrokeScale
local.LineWidth = &width
}
}
object, err := e.styleObject(node.object, local)
if err != nil {
return err
}
if err := node.update(object); err != nil {
return err
}
}
return nil
})
}
// AlignCompositeObjects 将内部多选成员对齐至选区边界,单选时对齐页面
// 入参: page 页面索引, path 父路径, indexes 成员序号, alignment 为left、center、right、top、middle或bottom
// 返回: error 错误信息
func (e *Editor) AlignCompositeObjects(page int, path ObjectPath, indexes []int, alignment string) error {
if !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, alignment) {
return fmt.Errorf("invalid alignment %q", alignment)
}
return e.arrangeCompositeObjects(page, path, indexes, func(boxes []Box) ([]Matrix, error) {
target, _ := ParseBox(e.pages[page].Area.PhysicalBox)
if len(boxes) > 1 {
target = Box{}
for _, box := range boxes {
target = unionTextBox(target, box)
}
}
matrices := make([]Matrix, len(boxes))
for i, box := range boxes {
matrices[i] = editorAlignment(box, target, alignment)
}
return matrices, nil
})
}
// DistributeCompositeObjects 按可见范围等距分布内部成员,固定两端并保留绘制顺序
// 入参: page 页面索引, path 父路径, indexes 成员序号, axis 为horizontal或vertical
// 返回: error 错误信息
func (e *Editor) DistributeCompositeObjects(page int, path ObjectPath, indexes []int, axis string) error {
if axis != "horizontal" && axis != "vertical" {
return fmt.Errorf("invalid distribution axis %q", axis)
}
return e.arrangeCompositeObjects(page, path, indexes, func(boxes []Box) ([]Matrix, error) {
positions := make([]editorObjectPosition, len(indexes))
for i, index := range indexes {
positions[i].index = index
}
return editorDistribution(boxes, positions, axis)
})
}
// arrangeCompositeObjects 按成员实际轮廓计算独立页面变换并提交一次历史记录 // arrangeCompositeObjects 按成员实际轮廓计算独立页面变换并提交一次历史记录
// 入参: page 页面索引, path 父路径, indexes 成员序号, arrange 排列计算 // 入参: page 页面索引, path 父路径, indexes 成员序号, arrange 排列计算
// 返回: error 错误信息 // 返回: error 错误信息
+140 -140
View File
@@ -53,6 +53,146 @@ func (e *Editor) PasteCompositeSelection(page int, selection *CompositeSelection
return result, err return result, err
} }
// CaptureCompositeObjects 捕获内部选区,不修改文档或分配资源
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号
// 返回: *CompositeSelection 独立快照, error 错误信息
func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []int) (*CompositeSelection, error) {
reader, _, root, nodes, err := e.compositeScope(page, path)
if err != nil {
return nil, err
}
defer reader.Close()
if len(indexes) == 0 {
return nil, fmt.Errorf("composite selection is empty")
}
indexes = slices.Clone(indexes)
slices.Sort(indexes)
var owner *editorCompositeNode
var container *editorXML
selection := &CompositeSelection{editor: e, page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: slices.Clone(path.Children)}}
if path.Annotation == "" {
selection.source = e.objectOrigin(path.ID).page
} else if e.source != nil {
selection.source = e.source.pages[e.pages[page].ID]
}
for j, i := range indexes {
if i < 0 || i >= len(nodes) || j > 0 && indexes[j-1] == i {
return nil, fmt.Errorf("invalid composite copy selection")
}
node := nodes[i]
if !node.transformable() || !editorXMLCopyable(node.node) {
return nil, fmt.Errorf("composite member cannot be copied")
}
if j == 0 {
owner, container = node.owner, node.span.parent
} else if node.span.parent != container {
return nil, fmt.Errorf("copy requires members in the same container")
}
copy := *node
copy.owner, copy.span = nil, nil
selection.nodes = append(selection.nodes, &copy)
}
scope := root.loadedScope(path.Children)
for scope != owner {
scope = scope.ref
selection.references++
}
for node := container; node != owner.node; node = node.parent {
containers := compositeContainers(node.parent)
selection.containers = append(selection.containers, slices.Index(containers, node))
}
slices.Reverse(selection.containers)
return selection, nil
}
// PasteCompositeObjects 将快照粘贴至内部范围末尾,不依赖源成员当前的序号或存在性
// 同范围沿用原容器和当前继承样式,跨范围固定对象的源外观,位移使用页面毫米
// 目标范围的父透明度及裁剪仍作用于新成员,全部成员提交一次撤销记录
// 入参: page 页面索引, path 父复合路径, selection 捕获的快照, dx、dy 页面位移
// 返回: []int 新成员序号, error 错误信息
func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *CompositeSelection, dx, dy float64) ([]int, error) {
if selection == nil || selection.editor != e {
return nil, fmt.Errorf("composite selection belongs to another editor")
}
if !finite(dx) || !finite(dy) {
return nil, fmt.Errorf("paste requires finite offsets")
}
if _, err := e.page(page); err != nil {
return nil, err
}
if e.pages[page].ID != selection.page || path.ID != selection.path.ID || path.Annotation != selection.path.Annotation || !slices.Equal(path.Children, selection.path.Children) {
var result []int
err := e.pasteCompositeSelection(selection, func(objects []GraphicObject) error {
var err error
result, err = e.CopyObjectsToComposite(page, path, objects, dx, dy)
return err
})
return result, err
}
var result []int
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, _ []*editorCompositeNode) error {
scope := root.loadedScope(path.Children)
owner := scope
for i := 0; i < selection.references; i++ {
owner = owner.ref
if owner == nil {
return fmt.Errorf("composite resource container no longer exists")
}
}
container := owner.node
for _, index := range selection.containers {
containers := compositeContainers(container)
if index >= len(containers) {
return fmt.Errorf("composite page block no longer exists")
}
container = containers[index]
}
box, _ := ParseBox(owner.object.CompositeGraphicUnit.Boundary)
parent := owner.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(owner.object.CompositeGraphicUnit.CTM))
nodes := make([]*editorCompositeNode, len(selection.nodes))
for i, node := range selection.nodes {
copy := *node
copy.parent, copy.boundaryInCTM = parent, owner.boundaryInCTM
nodes[i] = &copy
}
data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
if err != nil {
return err
}
maps.Copy(owner.states, states)
next, reached := 0, false
var count func(*editorCompositeNode)
count = func(node *editorCompositeNode) {
if node.ref != nil {
count(node.ref)
}
if reached {
return
}
for _, child := range node.children {
if node != owner || child.span.start < container.close {
next++
}
}
reached = node == owner
}
count(scope)
patch := editorXMLPatch{container.close, container.close, data}
if container.open == container.end {
patch = editorXMLContent(owner.data, container, data)
}
owner.patches = append(owner.patches, patch)
for i := range nodes {
result = append(result, next+i)
}
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
// pasteCompositeSelection 固定内部快照的页面几何和外观,粘贴失败时回收新增绘制资源 // pasteCompositeSelection 固定内部快照的页面几何和外观,粘贴失败时回收新增绘制资源
// 入参: selection 独立内部快照, paste 目标范围粘贴操作 // 入参: selection 独立内部快照, paste 目标范围粘贴操作
// 返回: error 错误信息 // 返回: error 错误信息
@@ -224,146 +364,6 @@ func (n *editorCompositeNode) convertCoordinates(parent Matrix, inward bool) err
return nil return nil
} }
// CaptureCompositeObjects 捕获内部选区,不修改文档或分配资源
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号
// 返回: *CompositeSelection 独立快照, error 错误信息
func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []int) (*CompositeSelection, error) {
reader, _, root, nodes, err := e.compositeScope(page, path)
if err != nil {
return nil, err
}
defer reader.Close()
if len(indexes) == 0 {
return nil, fmt.Errorf("composite selection is empty")
}
indexes = slices.Clone(indexes)
slices.Sort(indexes)
var owner *editorCompositeNode
var container *editorXML
selection := &CompositeSelection{editor: e, page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: slices.Clone(path.Children)}}
if path.Annotation == "" {
selection.source = e.objectOrigin(path.ID).page
} else if e.source != nil {
selection.source = e.source.pages[e.pages[page].ID]
}
for j, i := range indexes {
if i < 0 || i >= len(nodes) || j > 0 && indexes[j-1] == i {
return nil, fmt.Errorf("invalid composite copy selection")
}
node := nodes[i]
if !node.transformable() || !editorXMLCopyable(node.node) {
return nil, fmt.Errorf("composite member cannot be copied")
}
if j == 0 {
owner, container = node.owner, node.span.parent
} else if node.span.parent != container {
return nil, fmt.Errorf("copy requires members in the same container")
}
copy := *node
copy.owner, copy.span = nil, nil
selection.nodes = append(selection.nodes, &copy)
}
scope := root.loadedScope(path.Children)
for scope != owner {
scope = scope.ref
selection.references++
}
for node := container; node != owner.node; node = node.parent {
containers := compositeContainers(node.parent)
selection.containers = append(selection.containers, slices.Index(containers, node))
}
slices.Reverse(selection.containers)
return selection, nil
}
// PasteCompositeObjects 将快照粘贴至内部范围末尾,不依赖源成员当前的序号或存在性
// 同范围沿用原容器和当前继承样式,跨范围固定对象的源外观,位移使用页面毫米
// 目标范围的父透明度及裁剪仍作用于新成员,全部成员提交一次撤销记录
// 入参: page 页面索引, path 父复合路径, selection 捕获的快照, dx、dy 页面位移
// 返回: []int 新成员序号, error 错误信息
func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *CompositeSelection, dx, dy float64) ([]int, error) {
if selection == nil || selection.editor != e {
return nil, fmt.Errorf("composite selection belongs to another editor")
}
if !finite(dx) || !finite(dy) {
return nil, fmt.Errorf("paste requires finite offsets")
}
if _, err := e.page(page); err != nil {
return nil, err
}
if e.pages[page].ID != selection.page || path.ID != selection.path.ID || path.Annotation != selection.path.Annotation || !slices.Equal(path.Children, selection.path.Children) {
var result []int
err := e.pasteCompositeSelection(selection, func(objects []GraphicObject) error {
var err error
result, err = e.CopyObjectsToComposite(page, path, objects, dx, dy)
return err
})
return result, err
}
var result []int
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, _ []*editorCompositeNode) error {
scope := root.loadedScope(path.Children)
owner := scope
for i := 0; i < selection.references; i++ {
owner = owner.ref
if owner == nil {
return fmt.Errorf("composite resource container no longer exists")
}
}
container := owner.node
for _, index := range selection.containers {
containers := compositeContainers(container)
if index >= len(containers) {
return fmt.Errorf("composite page block no longer exists")
}
container = containers[index]
}
box, _ := ParseBox(owner.object.CompositeGraphicUnit.Boundary)
parent := owner.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(owner.object.CompositeGraphicUnit.CTM))
nodes := make([]*editorCompositeNode, len(selection.nodes))
for i, node := range selection.nodes {
copy := *node
copy.parent, copy.boundaryInCTM = parent, owner.boundaryInCTM
nodes[i] = &copy
}
data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
if err != nil {
return err
}
maps.Copy(owner.states, states)
next, reached := 0, false
var count func(*editorCompositeNode)
count = func(node *editorCompositeNode) {
if node.ref != nil {
count(node.ref)
}
if reached {
return
}
for _, child := range node.children {
if node != owner || child.span.start < container.close {
next++
}
}
reached = node == owner
}
count(scope)
patch := editorXMLPatch{container.close, container.close, data}
if container.open == container.end {
patch = editorXMLContent(owner.data, container, data)
}
owner.patches = append(owner.patches, patch)
for i := range nodes {
result = append(result, next+i)
}
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
// loadedScope 获取已由compositeScope展开并校验的范围 // loadedScope 获取已由compositeScope展开并校验的范围
// 入参: path 逐层成员序号 // 入参: path 逐层成员序号
// 返回: *editorCompositeNode 范围节点 // 返回: *editorCompositeNode 范围节点
+109 -109
View File
@@ -22,28 +22,6 @@ import (
"slices" "slices"
) )
// matrix 获取对象局部坐标到页面的变换,兼容旧式内联边界
// 入参: transform 是否包含对象CTM
// 返回: Matrix 坐标变换
func (n *editorCompositeNode) matrix(transform bool) Matrix {
boundary, ctm := editorGeometry(n.object)
box, _ := ParseBox(boundary)
m := IdentityMatrix
if transform {
m = NewMatrix(ctm)
if n.object.Type == "ImageObject" && ctm == "" {
m = Matrix{a: box.W, d: box.H}
}
}
if n.boundaryInCTM || n.object.Type == "CompositeObject" || n.object.Type == "CompositeGraphicUnit" {
return n.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(m)
}
if !transform {
return TranslationMatrix(box.X, box.Y)
}
return TranslationMatrix(box.X, box.Y).Multiply(n.parent).Multiply(m)
}
// CropCompositeImage 将内部图片与页面矩形取交集,保留原裁剪、蒙版、变换和边框 // CropCompositeImage 将内部图片与页面矩形取交集,保留原裁剪、蒙版、变换和边框
// 只收窄可见区域,撤销恢复上次裁剪,不重采样图片 // 只收窄可见区域,撤销恢复上次裁剪,不重采样图片
// 入参: page 页面索引, path 父复合路径, index 图片序号, box 页面毫米范围 // 入参: page 页面索引, path 父复合路径, index 图片序号, box 页面毫米范围
@@ -76,57 +54,6 @@ func (e *Editor) CropCompositeImage(page int, path ObjectPath, index int, box Bo
}) })
} }
// appendCompositeClip 追加独立交集裁剪并记录可还原序号,保留原始裁剪XML
// 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原序号
// 返回: error 错误信息
func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Matrix, track bool) error {
clip := Clip{Area: []ClipArea{{CTM: matrix.String(), Path: []PathObject{shape}}}}
if clips := *editorObjectClips(&node.object); clips != nil && slices.ContainsFunc(clips.Clip, func(previous Clip) bool { return reflect.DeepEqual(previous, clip) }) {
return nil
}
state := node.states[editorObjectID(node.object)]
if track {
crop := editorCompositeCrop{exists: node.node.child("Clips") != nil}
if state.crop != nil {
crop = *state.crop
}
count := 0
if clips := node.object.ImageObject.Clips; clips != nil {
count = len(clips.Clip)
}
crop.indexes = append(slices.Clone(crop.indexes), count)
state.crop = &crop
}
data, err := encodeOFDXML(func(x *ofdXML) {
x.clips(&Clips{Clip: []Clip{clip}})
})
if err != nil {
return err
}
data = bytes.TrimPrefix(data, []byte(xml.Header))
root, err := parseEditorXML(data)
if err != nil {
return err
}
var patch editorXMLPatch
if original := node.node.child("Clips"); original != nil {
clip := editorXMLEncodedFragment(data, root.children[0])
content := append(bytes.Clone(node.data[original.open:original.close]), clip...)
patch = editorXMLContent(node.data, original, content)
} else {
data = editorXMLEncodedFragment(data, root)
content := append(data, node.data[node.node.open:node.node.close]...)
patch = editorXMLContent(node.data, node.node, content)
}
updated, err := newEditorCompositeNode(editorPatchXML(node.data, []editorXMLPatch{patch}))
if err != nil {
return err
}
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
node.setState(state)
return nil
}
// ResetCompositeImageCrop 仅移除本次编辑追加的裁剪,保留输入文件中的裁剪、蒙版和其他属性 // ResetCompositeImageCrop 仅移除本次编辑追加的裁剪,保留输入文件中的裁剪、蒙版和其他属性
// 入参: page 页面索引, path 父路径, index 图片序号 // 入参: page 页面索引, path 父路径, index 图片序号
// 返回: error 错误信息 // 返回: error 错误信息
@@ -181,42 +108,6 @@ func (e *Editor) ResetImageCrop(page int, id string) error {
return e.updateObjectOrigins(page, []GraphicObject{node.object}, true, map[string]*editorObjectOrigin{id: next}) return e.updateObjectOrigins(page, []GraphicObject{node.object}, true, map[string]*editorObjectOrigin{id: next})
} }
// resetCompositeImageCrop 恢复图片原始裁剪节点,不还原后续变换和资源替换
// 入参: node 图片节点
// 返回: error 错误信息
func resetCompositeImageCrop(node *editorCompositeNode) error {
if node.object.Type != "ImageObject" {
return fmt.Errorf("composite member is not an image")
}
state := node.states[editorObjectID(node.object)]
if state.crop == nil {
return nil
}
clips := node.node.child("Clips")
var patches []editorXMLPatch
if !state.crop.exists && len(node.object.ImageObject.Clips.Clip) == len(state.crop.indexes) {
patches = append(patches, editorXMLPatch{clips.start, clips.end, nil})
} else {
count := 0
for _, child := range clips.children {
if child.name.Local == "Clip" {
if slices.Contains(state.crop.indexes, count) {
patches = append(patches, editorXMLPatch{child.start, child.end, nil})
}
count++
}
}
}
updated, err := newEditorCompositeNode(editorPatchXML(node.data, patches))
if err != nil {
return err
}
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
state.crop = nil
node.setState(state)
return nil
}
// FitCompositeImage 按原像素比例适应或填充内部图片框,保留原始裁剪和蒙版 // FitCompositeImage 按原像素比例适应或填充内部图片框,保留原始裁剪和蒙版
// 入参: page 页面索引, path 父路径, index 图片序号, mode 为contain或cover // 入参: page 页面索引, path 父路径, index 图片序号, mode 为contain或cover
// 返回: error 错误信息 // 返回: error 错误信息
@@ -256,3 +147,112 @@ func (e *Editor) FitCompositeImage(page int, path ObjectPath, index int, mode st
return appendCompositeClip(node, shape, inverse.Multiply(frame), true) return appendCompositeClip(node, shape, inverse.Multiply(frame), true)
}) })
} }
// matrix 获取对象局部坐标到页面的变换,兼容旧式内联边界
// 入参: transform 是否包含对象CTM
// 返回: Matrix 坐标变换
func (n *editorCompositeNode) matrix(transform bool) Matrix {
boundary, ctm := editorGeometry(n.object)
box, _ := ParseBox(boundary)
m := IdentityMatrix
if transform {
m = NewMatrix(ctm)
if n.object.Type == "ImageObject" && ctm == "" {
m = Matrix{a: box.W, d: box.H}
}
}
if n.boundaryInCTM || n.object.Type == "CompositeObject" || n.object.Type == "CompositeGraphicUnit" {
return n.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(m)
}
if !transform {
return TranslationMatrix(box.X, box.Y)
}
return TranslationMatrix(box.X, box.Y).Multiply(n.parent).Multiply(m)
}
// appendCompositeClip 追加独立交集裁剪并记录可还原序号,保留原始裁剪XML
// 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原序号
// 返回: error 错误信息
func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Matrix, track bool) error {
clip := Clip{Area: []ClipArea{{CTM: matrix.String(), Path: []PathObject{shape}}}}
if clips := *editorObjectClips(&node.object); clips != nil && slices.ContainsFunc(clips.Clip, func(previous Clip) bool { return reflect.DeepEqual(previous, clip) }) {
return nil
}
state := node.states[editorObjectID(node.object)]
if track {
crop := editorCompositeCrop{exists: node.node.child("Clips") != nil}
if state.crop != nil {
crop = *state.crop
}
count := 0
if clips := node.object.ImageObject.Clips; clips != nil {
count = len(clips.Clip)
}
crop.indexes = append(slices.Clone(crop.indexes), count)
state.crop = &crop
}
data, err := encodeOFDXML(func(x *ofdXML) {
x.clips(&Clips{Clip: []Clip{clip}})
})
if err != nil {
return err
}
data = bytes.TrimPrefix(data, []byte(xml.Header))
root, err := parseEditorXML(data)
if err != nil {
return err
}
var patch editorXMLPatch
if original := node.node.child("Clips"); original != nil {
clip := editorXMLEncodedFragment(data, root.children[0])
content := append(bytes.Clone(node.data[original.open:original.close]), clip...)
patch = editorXMLContent(node.data, original, content)
} else {
data = editorXMLEncodedFragment(data, root)
content := append(data, node.data[node.node.open:node.node.close]...)
patch = editorXMLContent(node.data, node.node, content)
}
updated, err := newEditorCompositeNode(editorPatchXML(node.data, []editorXMLPatch{patch}))
if err != nil {
return err
}
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
node.setState(state)
return nil
}
// resetCompositeImageCrop 恢复图片原始裁剪节点,不还原后续变换和资源替换
// 入参: node 图片节点
// 返回: error 错误信息
func resetCompositeImageCrop(node *editorCompositeNode) error {
if node.object.Type != "ImageObject" {
return fmt.Errorf("composite member is not an image")
}
state := node.states[editorObjectID(node.object)]
if state.crop == nil {
return nil
}
clips := node.node.child("Clips")
var patches []editorXMLPatch
if !state.crop.exists && len(node.object.ImageObject.Clips.Clip) == len(state.crop.indexes) {
patches = append(patches, editorXMLPatch{clips.start, clips.end, nil})
} else {
count := 0
for _, child := range clips.children {
if child.name.Local == "Clip" {
if slices.Contains(state.crop.indexes, count) {
patches = append(patches, editorXMLPatch{child.start, child.end, nil})
}
count++
}
}
}
updated, err := newEditorCompositeNode(editorPatchXML(node.data, patches))
if err != nil {
return err
}
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
state.crop = nil
node.setState(state)
return nil
}
+72 -72
View File
@@ -21,18 +21,6 @@ import (
"slices" "slices"
) )
// siblings 获取同一原始容器内的直接绘制成员,不跨越资源和页块
// 返回: []*editorCompositeNode 按绘制顺序排列的成员
func (n *editorCompositeNode) siblings() []*editorCompositeNode {
var result []*editorCompositeNode
for _, member := range n.owner.children {
if member.span.parent == n.span.parent {
result = append(result, member)
}
}
return result
}
// DeleteCompositeObjects 删除内部成员,保留容器、未选对象及其他实例,一次撤销恢复全部 // DeleteCompositeObjects 删除内部成员,保留容器、未选对象及其他实例,一次撤销恢复全部
// 入参: page 页面索引, path 父复合路径, indexes 成员序号 // 入参: page 页面索引, path 父复合路径, indexes 成员序号
// 返回: error 错误信息 // 返回: error 错误信息
@@ -85,33 +73,6 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int,
return result, nil return result, nil
} }
// copyCompositeNodes 复制成员原文并统一重映射标识,保持副本之间的引用
// 入参: renderer 渲染器, nodes 快照, dx、dy 页面位移
// 返回: []byte 副本XML, map[string]editorCompositeState 会话信息, error 错误信息
func (e *Editor) copyCompositeNodes(renderer *Renderer, nodes []*editorCompositeNode, dx, dy float64) ([]byte, map[string]editorCompositeState, error) {
ids := make(map[string]string)
for _, node := range nodes {
if err := e.collectCompositeIDs(node.node, ids); err != nil {
return nil, nil, err
}
}
var data []byte
states := make(map[string]editorCompositeState)
for _, node := range nodes {
copy := *node
if err := e.transformCompositeMember(renderer, &copy, TranslationMatrix(dx, dy)); err != nil {
return nil, nil, err
}
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
if err != nil {
return nil, nil, err
}
maps.Copy(states, remapCompositeStates(node.states, ids))
data = append(data, fragment...)
}
return data, states, nil
}
// OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例 // OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, order 为up、down、top或bottom // 入参: page 页面索引, path 父复合路径, indexes 成员序号, order 为up、down、top或bottom
// 返回: []int 调整后的选区序号, error 错误信息 // 返回: []int 调整后的选区序号, error 错误信息
@@ -167,22 +128,6 @@ func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, dat
}) })
} }
// update 合并成员属性变化,不重写原文中未修改的字段
// 入参: object 修改后的对象
// 返回: error 错误信息
func (n *editorCompositeNode) update(object GraphicObject) error {
data, err := editorXMLObject(n.data, n.node, n.object, object)
if err != nil || bytes.Equal(data, n.data) {
return err
}
next, err := newEditorCompositeNode(data)
if err != nil {
return err
}
n.data, n.node, n.object, n.changed = data, next.node, next.object, true
return nil
}
// Shape 获取内部基本路径的页面几何,矩形和椭圆要求轴对齐,其他成员返回空类型 // Shape 获取内部基本路径的页面几何,矩形和椭圆要求轴对齐,其他成员返回空类型
// 返回: ShapeKind 图形类型, Box 几何范围,直线使用起点及有符号端点位移 // 返回: ShapeKind 图形类型, Box 几何范围,直线使用起点及有符号端点位移
func (m CompositeMember) Shape() (ShapeKind, Box) { func (m CompositeMember) Shape() (ShapeKind, Box) {
@@ -249,6 +194,78 @@ func (e *Editor) ReshapeCompositeFrame(page int, path ObjectPath, index int, box
}) })
} }
// ReshapeCompositeLine 修改内部直线的页面端点及箭头,保留原始属性和父变换
// 入参: page 页面索引, path 父路径, index 成员序号, kind 直线类型, box 页面端点
// 返回: error 错误信息
func (e *Editor) ReshapeCompositeLine(page int, path ObjectPath, index int, kind ShapeKind, box Box) error {
if kind != "" && !lineShapeKind(kind) {
return fmt.Errorf("unsupported line kind %q", kind)
}
return e.reshapeCompositeObject(page, path, index, kind, box, true)
}
// ReshapeCompositeObject 调整内部基本路径的页面几何,保留绘制样式、变换与裁剪
// 入参: page 页面索引, path 父路径, index 成员序号, box 页面范围,直线使用有符号端点位移
// 返回: error 错误信息
func (e *Editor) ReshapeCompositeObject(page int, path ObjectPath, index int, box Box) error {
return e.reshapeCompositeObject(page, path, index, "", box, false)
}
// siblings 获取同一原始容器内的直接绘制成员,不跨越资源和页块
// 返回: []*editorCompositeNode 按绘制顺序排列的成员
func (n *editorCompositeNode) siblings() []*editorCompositeNode {
var result []*editorCompositeNode
for _, member := range n.owner.children {
if member.span.parent == n.span.parent {
result = append(result, member)
}
}
return result
}
// copyCompositeNodes 复制成员原文并统一重映射标识,保持副本之间的引用
// 入参: renderer 渲染器, nodes 快照, dx、dy 页面位移
// 返回: []byte 副本XML, map[string]editorCompositeState 会话信息, error 错误信息
func (e *Editor) copyCompositeNodes(renderer *Renderer, nodes []*editorCompositeNode, dx, dy float64) ([]byte, map[string]editorCompositeState, error) {
ids := make(map[string]string)
for _, node := range nodes {
if err := e.collectCompositeIDs(node.node, ids); err != nil {
return nil, nil, err
}
}
var data []byte
states := make(map[string]editorCompositeState)
for _, node := range nodes {
copy := *node
if err := e.transformCompositeMember(renderer, &copy, TranslationMatrix(dx, dy)); err != nil {
return nil, nil, err
}
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
if err != nil {
return nil, nil, err
}
maps.Copy(states, remapCompositeStates(node.states, ids))
data = append(data, fragment...)
}
return data, states, nil
}
// update 合并成员属性变化,不重写原文中未修改的字段
// 入参: object 修改后的对象
// 返回: error 错误信息
func (n *editorCompositeNode) update(object GraphicObject) error {
data, err := editorXMLObject(n.data, n.node, n.object, object)
if err != nil || bytes.Equal(data, n.data) {
return err
}
next, err := newEditorCompositeNode(data)
if err != nil {
return err
}
n.data, n.node, n.object, n.changed = data, next.node, next.object, true
return nil
}
// shapeParent 获取路径所在坐标到页面的变换,兼容旧式内联边界 // shapeParent 获取路径所在坐标到页面的变换,兼容旧式内联边界
// 返回: Matrix 路径父坐标变换, bool 是否可逆 // 返回: Matrix 路径父坐标变换, bool 是否可逆
func (m CompositeMember) shapeParent() (Matrix, bool) { func (m CompositeMember) shapeParent() (Matrix, bool) {
@@ -267,23 +284,6 @@ func transformLineBox(box Box, matrix Matrix) Box {
return Box{X: x, Y: y, W: w, H: h} return Box{X: x, Y: y, W: w, H: h}
} }
// ReshapeCompositeLine 修改内部直线的页面端点及箭头,保留原始属性和父变换
// 入参: page 页面索引, path 父路径, index 成员序号, kind 直线类型, box 页面端点
// 返回: error 错误信息
func (e *Editor) ReshapeCompositeLine(page int, path ObjectPath, index int, kind ShapeKind, box Box) error {
if kind != "" && !lineShapeKind(kind) {
return fmt.Errorf("unsupported line kind %q", kind)
}
return e.reshapeCompositeObject(page, path, index, kind, box, true)
}
// ReshapeCompositeObject 调整内部基本路径的页面几何,保留绘制样式、变换与裁剪
// 入参: page 页面索引, path 父路径, index 成员序号, box 页面范围,直线使用有符号端点位移
// 返回: error 错误信息
func (e *Editor) ReshapeCompositeObject(page int, path ObjectPath, index int, box Box) error {
return e.reshapeCompositeObject(page, path, index, "", box, false)
}
// reshapeCompositeObject 按页面几何修改单个内部路径,可选修改箭头类型 // reshapeCompositeObject 按页面几何修改单个内部路径,可选修改箭头类型
// 入参: page 页面索引, path 父路径, index 成员序号, kind 目标类型,空值保持原类型, box 页面几何, lineOnly 是否仅接受直线 // 入参: page 页面索引, path 父路径, index 成员序号, kind 目标类型,空值保持原类型, box 页面几何, lineOnly 是否仅接受直线
// 返回: error 错误信息 // 返回: error 错误信息
+23 -23
View File
@@ -50,6 +50,29 @@ func (e *Editor) EraseObjectsPath(page int, ids []string, points []Point) error
return e.eraseObjects(page, ids, bounds, points, region) return e.eraseObjects(page, ids, bounds, points, region)
} }
// EraseCompositeObjects 擦除内部成员与矩形相交的部分,保留原始属性及共享资源
// 全部覆盖时删除成员,部分覆盖追加标准裁剪,不用于敏感信息脱敏
// 入参: page 页面索引, path 父路径, indexes 成员序号, box 页面毫米范围
// 返回: error 错误信息
func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
if _, err := creationBox(editorBoxString(box)); err != nil {
return err
}
return e.eraseCompositeObjects(page, path, indexes, geometryRectangle(box))
}
// EraseCompositeObjectsPath 按闭合折线擦除内部成员,末点自动连接起点
// 与矩形擦除共用原子提交和撤销逻辑,不用于敏感信息脱敏
// 入参: page 页面索引, path 父路径, indexes 成员序号, points 页面毫米坐标
// 返回: error 错误信息
func (e *Editor) EraseCompositeObjectsPath(page int, path ObjectPath, indexes []int, points []Point) error {
region, err := e.editorEraseRegion(points)
if err != nil || len(region) == 0 {
return err
}
return e.eraseCompositeObjects(page, path, indexes, region)
}
// editorEraseRegion 校验闭合折线并按奇偶规则构建擦除区域 // editorEraseRegion 校验闭合折线并按奇偶规则构建擦除区域
// 入参: points 页面毫米坐标 // 入参: points 页面毫米坐标
// 返回: GeometryPath 擦除区域, error 错误信息 // 返回: GeometryPath 擦除区域, error 错误信息
@@ -76,29 +99,6 @@ func (e *Editor) editorEraseRegion(points []Point) (GeometryPath, error) {
return geometry.Normalize(region, true) return geometry.Normalize(region, true)
} }
// EraseCompositeObjects 擦除内部成员与矩形相交的部分,保留原始属性及共享资源
// 全部覆盖时删除成员,部分覆盖追加标准裁剪,不用于敏感信息脱敏
// 入参: page 页面索引, path 父路径, indexes 成员序号, box 页面毫米范围
// 返回: error 错误信息
func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
if _, err := creationBox(editorBoxString(box)); err != nil {
return err
}
return e.eraseCompositeObjects(page, path, indexes, geometryRectangle(box))
}
// EraseCompositeObjectsPath 按闭合折线擦除内部成员,末点自动连接起点
// 与矩形擦除共用原子提交和撤销逻辑,不用于敏感信息脱敏
// 入参: page 页面索引, path 父路径, indexes 成员序号, points 页面毫米坐标
// 返回: error 错误信息
func (e *Editor) EraseCompositeObjectsPath(page int, path ObjectPath, indexes []int, points []Point) error {
region, err := e.editorEraseRegion(points)
if err != nil || len(region) == 0 {
return err
}
return e.eraseCompositeObjects(page, path, indexes, region)
}
// eraseCompositeObjects 按页面区域裁剪内部成员,全部成功后提交一次历史 // eraseCompositeObjects 按页面区域裁剪内部成员,全部成功后提交一次历史
// 入参: page 页面索引, path 父路径, indexes 成员序号, region 擦除区域 // 入参: page 页面索引, path 父路径, indexes 成员序号, region 擦除区域
// 返回: error 错误信息 // 返回: error 错误信息
+3 -3
View File
@@ -16,9 +16,6 @@ package ofdgo
import "errors" import "errors"
// EditReason 编辑操作受限的原因,不依赖错误描述的语言或文案
type EditReason string
const ( const (
// EditUnsupportedObject 对象包含尚不支持编辑的特性 // EditUnsupportedObject 对象包含尚不支持编辑的特性
EditUnsupportedObject EditReason = "unsupportedObject" EditUnsupportedObject EditReason = "unsupportedObject"
@@ -38,6 +35,9 @@ const (
EditInvalidObject EditReason = "invalidObject" EditInvalidObject EditReason = "invalidObject"
) )
// EditReason 编辑操作受限的原因,不依赖错误描述的语言或文案
type EditReason string
// EditError 编辑受限原因及原始错误,可通过errors.As取得Code和更具体的诊断 // EditError 编辑受限原因及原始错误,可通过errors.As取得Code和更具体的诊断
type EditError struct { type EditError struct {
Code EditReason Code EditReason
+47 -47
View File
@@ -19,6 +19,14 @@ import (
"slices" "slices"
) )
// editorChange 页面或对象操作及其修订标识
type editorChange struct {
undo func(*Editor)
redo func(*Editor)
before uint64
after uint64
}
// Transaction 将多项修改作为一次原子操作提交,失败时保留原文档及历史 // Transaction 将多项修改作为一次原子操作提交,失败时保留原文档及历史
// 回调仅操作传入的编辑器,不可保留该实例供后续使用 // 回调仅操作传入的编辑器,不可保留该实例供后续使用
// 入参: edit 批量修改回调 // 入参: edit 批量修改回调
@@ -49,53 +57,6 @@ func (e *Editor) Transaction(edit func(*Editor) error) error {
return nil return nil
} }
// transactionSnapshot 复制可变容器,保留只读XML、图元及二进制资源的共享
// 返回: Editor 独立事务状态
func (e *Editor) transactionSnapshot() Editor {
next := *e
next.Info = cloneEditorData(e.Info)
next.pages = make([]PageContent, len(e.pages))
for i, page := range e.pages {
next.pages[i] = copyEditorPage(page)
}
next.resources = slices.Clone(e.resources)
next.fonts, next.images, next.resourceID = maps.Clone(e.fonts), maps.Clone(e.images), maps.Clone(e.resourceID)
next.origins = maps.Clone(e.origins)
next.removedPages = maps.Clone(e.removedPages)
next.fontMetrics = maps.Clone(e.fontMetrics)
if e.source != nil {
source := *e.source
source.pages, source.origins = maps.Clone(source.pages), maps.Clone(source.origins)
next.source = &source
}
return next
}
// restoreTransaction 恢复文档状态并保留历史容器、共享资源和递增标识
// 入参: state 文档状态
func (e *Editor) restoreTransaction(state Editor) {
state = state.transactionSnapshot()
if len(e.resources) > len(state.resources) {
state.resources = append(state.resources, e.resources[len(state.resources):]...)
}
maps.Copy(state.fonts, e.fonts)
maps.Copy(state.images, e.images)
maps.Copy(state.resourceID, e.resourceID)
state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit
state.serial, state.maxID = max(e.serial, state.serial), max(e.maxID, state.maxID)
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.fontRenderer, state.fontMetrics = e.fontRenderer, e.fontMetrics
*e = state
}
// editorChange 页面或对象操作及其修订标识
type editorChange struct {
undo func(*Editor)
redo func(*Editor)
before uint64
after uint64
}
// SetHistoryLimit 设置撤销和重做记录的总上限,默认关闭,非正数关闭并释放记录 // SetHistoryLimit 设置撤销和重做记录的总上限,默认关闭,非正数关闭并释放记录
// 缩减上限时优先保留最近的撤销记录,不影响当前文档及修订标识 // 缩减上限时优先保留最近的撤销记录,不影响当前文档及修订标识
// 记录页面、对象及SetInfo操作,不包含Info的直接修改及字体、图片注册,资源继续共享 // 记录页面、对象及SetInfo操作,不包含Info的直接修改及字体、图片注册,资源继续共享
@@ -160,6 +121,45 @@ func (e *Editor) Revision() uint64 {
return e.revision return e.revision
} }
// transactionSnapshot 复制可变容器,保留只读XML、图元及二进制资源的共享
// 返回: Editor 独立事务状态
func (e *Editor) transactionSnapshot() Editor {
next := *e
next.Info = cloneEditorData(e.Info)
next.pages = make([]PageContent, len(e.pages))
for i, page := range e.pages {
next.pages[i] = copyEditorPage(page)
}
next.resources = slices.Clone(e.resources)
next.fonts, next.images, next.resourceID = maps.Clone(e.fonts), maps.Clone(e.images), maps.Clone(e.resourceID)
next.origins = maps.Clone(e.origins)
next.removedPages = maps.Clone(e.removedPages)
next.fontMetrics = maps.Clone(e.fontMetrics)
if e.source != nil {
source := *e.source
source.pages, source.origins = maps.Clone(source.pages), maps.Clone(source.origins)
next.source = &source
}
return next
}
// restoreTransaction 恢复文档状态并保留历史容器、共享资源和递增标识
// 入参: state 文档状态
func (e *Editor) restoreTransaction(state Editor) {
state = state.transactionSnapshot()
if len(e.resources) > len(state.resources) {
state.resources = append(state.resources, e.resources[len(state.resources):]...)
}
maps.Copy(state.fonts, e.fonts)
maps.Copy(state.images, e.images)
maps.Copy(state.resourceID, e.resourceID)
state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit
state.serial, state.maxID = max(e.serial, state.serial), max(e.maxID, state.maxID)
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.fontRenderer, state.fontMetrics = e.fontRenderer, e.fontMetrics
*e = state
}
// recordChange 分配修订标识并为已完成的有效修改预留记录 // recordChange 分配修订标识并为已完成的有效修改预留记录
// 返回: *editorChange 记录,未启用历史时为nil // 返回: *editorChange 记录,未启用历史时为nil
func (e *Editor) recordChange() *editorChange { func (e *Editor) recordChange() *editorChange {
+20 -20
View File
@@ -78,6 +78,14 @@ type ObjectImportOptions struct {
OnProgress func(stage string, completed, total int) error OnProgress func(stage string, completed, total int) error
} }
// editorProgressReader 为输入流提供可取消的分块读取检查点
type editorProgressReader struct {
io.Reader
progress editorProgress
stage string
completed int
}
// ImportObjects 迁移选区及引用资源,不改变来源与目标页面结构,提交一次撤销记录 // ImportObjects 迁移选区及引用资源,不改变来源与目标页面结构,提交一次撤销记录
// 保留对象原文与图层继承,同页链接映射到目标页,无法迁移的跨页链接明确报错 // 保留对象原文与图层继承,同页链接映射到目标页,无法迁移的跨页链接明确报错
// 返回后可关闭来源阅读器,后续修改来源不影响副本 // 返回后可关闭来源阅读器,后续修改来源不影响副本
@@ -238,6 +246,18 @@ func (e *Editor) ImportPagesWithOptions(source *Reader, indexes []int, at int, o
return e.importPages(source, indexes, at, false, options) return e.importPages(source, indexes, at, false, options)
} }
// Read 读取数据并在下一块开始前检查取消
// 入参: data 接收缓冲区
// 返回: int 读取字节数, error 错误信息
func (r *editorProgressReader) Read(data []byte) (int, error) {
if err := r.progress.report(r.stage, r.completed, 0); err != nil {
return 0, err
}
n, err := r.Reader.Read(data[:min(len(data), 1<<20)])
r.completed += n
return n, err
}
// importPages 迁移页面,同文档复制时复用原资源并保留其他页面的跳转 // importPages 迁移页面,同文档复制时复用原资源并保留其他页面的跳转
// 入参: source 来源阅读器, indexes 来源页索引, at 插入位置, copyPage 是否同文档复制, options 导入选项 // 入参: source 来源阅读器, indexes 来源页索引, at 插入位置, copyPage 是否同文档复制, options 导入选项
// 返回: []string 新页标识, error 错误信息 // 返回: []string 新页标识, error 错误信息
@@ -507,26 +527,6 @@ func (m *editorPageImport) readFile(name string) ([]byte, error) {
return io.ReadAll(&editorProgressReader{Reader: input, progress: m.progress, stage: "resources"}) return io.ReadAll(&editorProgressReader{Reader: input, progress: m.progress, stage: "resources"})
} }
// editorProgressReader 为输入流提供可取消的分块读取检查点
type editorProgressReader struct {
io.Reader
progress editorProgress
stage string
completed int
}
// Read 读取数据并在下一块开始前检查取消
// 入参: data 接收缓冲区
// 返回: int 读取字节数, error 错误信息
func (r *editorProgressReader) Read(data []byte) (int, error) {
if err := r.progress.report(r.stage, r.completed, 0); err != nil {
return 0, err
}
n, err := r.Reader.Read(data[:min(len(data), 1<<20)])
r.completed += n
return n, err
}
// outlines 筛选指向导入页面的目录树并复用标准动作迁移 // outlines 筛选指向导入页面的目录树并复用标准动作迁移
// 返回: []byte 目录片段, error 错误信息 // 返回: []byte 目录片段, error 错误信息
func (m *editorPageImport) outlines() ([]byte, error) { func (m *editorPageImport) outlines() ([]byte, error) {
+100 -100
View File
@@ -111,76 +111,6 @@ func (e *Editor) SetObjectActions(page int, ids []string, actions []Action) erro
return e.updateObjects(page, objects, true) return e.updateObjects(page, objects, true)
} }
// validateObjectActions 校验新建对象的动作结构,目标不必可达
// 入参: actions 动作列表
// 返回: error 错误信息
func validateObjectActions(actions []Action) error {
for _, action := range actions {
if !slices.Contains([]string{"CLICK", "DO", "PO"}, action.Event) {
return fmt.Errorf("unsupported action event %q", action.Event)
}
count := 0
for _, present := range []bool{action.URI != nil, action.Goto != nil, action.GotoA != nil, action.Sound != nil, action.Movie != nil} {
if present {
count++
}
}
if count != 1 {
return fmt.Errorf("specify one action target")
}
if action.GotoA != nil && action.GotoA.AttachID == "" {
return fmt.Errorf("attachment action requires an attachment ID")
}
if sound := action.Sound; sound != nil {
if sound.ResourceID == "" || sound.Volume != nil && (*sound.Volume < 0 || *sound.Volume > 100) {
return fmt.Errorf("invalid sound action resource or volume")
}
}
if movie := action.Movie; movie != nil {
if movie.ResourceID == "" || !slices.Contains([]string{"", "Play", "Stop", "Pause", "Resume"}, movie.Operator) {
return fmt.Errorf("invalid movie action resource or operator")
}
}
if action.Goto != nil {
if (action.Goto.Dest == nil) == (action.Goto.Bookmark == nil) {
return fmt.Errorf("specify one destination or bookmark")
}
if action.Goto.Dest != nil {
if _, err := annotationLinkXML(AnnotationLink{Dest: action.Goto.Dest}); err != nil {
return err
}
}
}
if action.Region != nil {
for _, area := range action.Region.Area {
if _, err := creationNumbers(area.Start, 2); err != nil {
return err
}
for _, command := range area.Command {
var points []string
switch command.Type {
case "Line":
points = []string{command.Point1}
case "QuadraticBezier":
points = []string{command.Point1, command.Point2}
case "CubicBezier":
points = []string{command.Point1, command.Point2, command.Point3}
case "Close":
default:
return fmt.Errorf("unsupported new action region command %q", command.Type)
}
for _, point := range points {
if _, err := creationNumbers(point, 2); err != nil {
return err
}
}
}
}
}
}
return nil
}
// AnnotationLink 读取注解中唯一的点击跳转,书签目标保持原始动作 // AnnotationLink 读取注解中唯一的点击跳转,书签目标保持原始动作
// 入参: page 页面索引, id 注解标识 // 入参: page 页面索引, id 注解标识
// 返回: Action 跳转动作, error 错误信息 // 返回: Action 跳转动作, error 错误信息
@@ -203,36 +133,6 @@ func (e *Editor) AnnotationLink(page int, id string) (Action, error) {
return action, err return action, err
} }
// annotationLinkNodes 获取外观内的点击跳转和基本对象,不进入私有扩展
// 入参: root 注解根节点
// 返回: []*editorXML 跳转目标, []*editorXML 基本对象
func annotationLinkNodes(root *editorXML) ([]*editorXML, []*editorXML) {
var targets, objects []*editorXML
var walk func(*editorXML)
walk = func(n *editorXML) {
if n.name.Space != root.name.Space {
return
}
if n.name.Local == "Action" && n.attr("Event") == "CLICK" {
for _, child := range n.children {
if child.name.Space == n.name.Space && (child.name.Local == "URI" || child.name.Local == "Goto") {
targets = append(targets, child)
}
}
}
if slices.Contains([]string{"TextObject", "PathObject", "ImageObject"}, n.name.Local) {
objects = append(objects, n)
}
for _, child := range n.children {
walk(child)
}
}
if appearance := root.child("Appearance"); appearance != nil {
walk(appearance)
}
return targets, objects
}
// AddLinkAnnotation 新增标准链接注解,区域位于页面毫米坐标,不检查目标是否可达 // AddLinkAnnotation 新增标准链接注解,区域位于页面毫米坐标,不检查目标是否可达
// 入参: page 页面索引, box 点击区域, target 链接目标, creator 作者 // 入参: page 页面索引, box 点击区域, target 链接目标, creator 作者
// 返回: string 注解标识, error 错误信息 // 返回: string 注解标识, error 错误信息
@@ -362,6 +262,106 @@ func (e *Editor) UpdateAnnotationLink(page int, id string, target AnnotationLink
}) })
} }
// validateObjectActions 校验新建对象的动作结构,目标不必可达
// 入参: actions 动作列表
// 返回: error 错误信息
func validateObjectActions(actions []Action) error {
for _, action := range actions {
if !slices.Contains([]string{"CLICK", "DO", "PO"}, action.Event) {
return fmt.Errorf("unsupported action event %q", action.Event)
}
count := 0
for _, present := range []bool{action.URI != nil, action.Goto != nil, action.GotoA != nil, action.Sound != nil, action.Movie != nil} {
if present {
count++
}
}
if count != 1 {
return fmt.Errorf("specify one action target")
}
if action.GotoA != nil && action.GotoA.AttachID == "" {
return fmt.Errorf("attachment action requires an attachment ID")
}
if sound := action.Sound; sound != nil {
if sound.ResourceID == "" || sound.Volume != nil && (*sound.Volume < 0 || *sound.Volume > 100) {
return fmt.Errorf("invalid sound action resource or volume")
}
}
if movie := action.Movie; movie != nil {
if movie.ResourceID == "" || !slices.Contains([]string{"", "Play", "Stop", "Pause", "Resume"}, movie.Operator) {
return fmt.Errorf("invalid movie action resource or operator")
}
}
if action.Goto != nil {
if (action.Goto.Dest == nil) == (action.Goto.Bookmark == nil) {
return fmt.Errorf("specify one destination or bookmark")
}
if action.Goto.Dest != nil {
if _, err := annotationLinkXML(AnnotationLink{Dest: action.Goto.Dest}); err != nil {
return err
}
}
}
if action.Region != nil {
for _, area := range action.Region.Area {
if _, err := creationNumbers(area.Start, 2); err != nil {
return err
}
for _, command := range area.Command {
var points []string
switch command.Type {
case "Line":
points = []string{command.Point1}
case "QuadraticBezier":
points = []string{command.Point1, command.Point2}
case "CubicBezier":
points = []string{command.Point1, command.Point2, command.Point3}
case "Close":
default:
return fmt.Errorf("unsupported new action region command %q", command.Type)
}
for _, point := range points {
if _, err := creationNumbers(point, 2); err != nil {
return err
}
}
}
}
}
}
return nil
}
// annotationLinkNodes 获取外观内的点击跳转和基本对象,不进入私有扩展
// 入参: root 注解根节点
// 返回: []*editorXML 跳转目标, []*editorXML 基本对象
func annotationLinkNodes(root *editorXML) ([]*editorXML, []*editorXML) {
var targets, objects []*editorXML
var walk func(*editorXML)
walk = func(n *editorXML) {
if n.name.Space != root.name.Space {
return
}
if n.name.Local == "Action" && n.attr("Event") == "CLICK" {
for _, child := range n.children {
if child.name.Space == n.name.Space && (child.name.Local == "URI" || child.name.Local == "Goto") {
targets = append(targets, child)
}
}
}
if slices.Contains([]string{"TextObject", "PathObject", "ImageObject"}, n.name.Local) {
objects = append(objects, n)
}
for _, child := range n.children {
walk(child)
}
}
if appearance := root.child("Appearance"); appearance != nil {
walk(appearance)
}
return targets, objects
}
// annotationLinkXML 序列化标准跳转目标,不改写其他动作或命名空间 // annotationLinkXML 序列化标准跳转目标,不改写其他动作或命名空间
// 入参: target 跳转目标 // 入参: target 跳转目标
// 返回: []byte 独立XML片段, error 错误信息 // 返回: []byte 独立XML片段, error 错误信息
+54 -54
View File
@@ -98,16 +98,6 @@ type ObjectCapabilities struct {
MissingGlyphs *MissingGlyphError MissingGlyphs *MissingGlyphError
} }
// editError 将操作受限原因转为错误,保留可供调用方识别的缺字诊断
// 返回: error 受限原因
func (c ObjectCapabilities) editError() error {
var err error = errors.New(c.Reason)
if c.MissingGlyphs != nil {
err = c.MissingGlyphs
}
return &EditError{Code: c.ReasonCode, Err: err}
}
// PageCapabilities 页面可执行的操作,Insert表示可以添加RGB对象;新增空白页面始终可用 // PageCapabilities 页面可执行的操作,Insert表示可以添加RGB对象;新增空白页面始终可用
type PageCapabilities struct { type PageCapabilities struct {
Insert bool Insert bool
@@ -159,6 +149,60 @@ func (r *Reader) Editor() (*Editor, error) {
return e, nil return e, nil
} }
// PageCapabilities 获取页面操作范围,复制和删除由库统一维护标准引用
// 入参: index 页面索引
// 返回: PageCapabilities 操作能力, error 错误信息
func (e *Editor) PageCapabilities(index int) (PageCapabilities, error) {
if index < 0 || index >= len(e.pages) {
return PageCapabilities{}, fmt.Errorf("page index %d out of range", index)
}
return PageCapabilities{Insert: e.sourceRGB(), Copy: true, Delete: true, Move: true, Resize: true}, nil
}
// ObjectCapabilities 获取对象可执行的操作,不支持的内容保持原文,不强制转换
// 字体缺失或子集不包含原文时仍可保真移动、复制和删除,不自动重新排版
// 显式提供可用字体后可通过UpdateObject替换;原文布局未知时不自动推断段落选项
// 入参: page 页面索引, id 对象标识
// 返回: ObjectCapabilities 操作能力, error 错误信息
func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, error) {
layer, index, err := e.findObject(page, id)
if err != nil {
return ObjectCapabilities{}, err
}
return e.objectCapabilities(layer.Objects[index], make(map[*editorXML]bool)), nil
}
// FontData 获取编辑使用的字体数据,外部字体不写入文档资源
// 入参: id 字体资源标识
// 返回: []byte 独立字体数据, error 错误信息
func (e *Editor) FontData(id string) ([]byte, error) {
for _, resource := range e.resources {
if resource.font != nil && resource.font.ID == id && len(resource.data) != 0 {
return bytes.Clone(resource.data), nil
}
}
if e.source != nil {
if definition := e.source.reader.fontCache[id]; definition != nil && definition.FontFile != "" {
return e.source.reader.FontData(id)
}
}
sfnt, err := e.editorFont(id)
if err != nil {
return nil, err
}
return sfnt.Write(), nil
}
// editError 将操作受限原因转为错误,保留可供调用方识别的缺字诊断
// 返回: error 受限原因
func (c ObjectCapabilities) editError() error {
var err error = errors.New(c.Reason)
if c.MissingGlyphs != nil {
err = c.MissingGlyphs
}
return &EditError{Code: c.ReasonCode, Err: err}
}
// cloneEditorReader 复用已验证的只读包索引,独立保存可变文档与读取缓存 // cloneEditorReader 复用已验证的只读包索引,独立保存可变文档与读取缓存
// 入参: reader 已加载主文档的阅读器 // 入参: reader 已加载主文档的阅读器
// 返回: *Reader 不拥有输入文件的独立阅读器 // 返回: *Reader 不拥有输入文件的独立阅读器
@@ -292,16 +336,6 @@ func (e *Editor) originalPage(index int) bool {
return e.source != nil && index >= 0 && index < len(e.pages) && e.source.pages[e.pages[index].ID] != nil return e.source != nil && index >= 0 && index < len(e.pages) && e.source.pages[e.pages[index].ID] != nil
} }
// PageCapabilities 获取页面操作范围,复制和删除由库统一维护标准引用
// 入参: index 页面索引
// 返回: PageCapabilities 操作能力, error 错误信息
func (e *Editor) PageCapabilities(index int) (PageCapabilities, error) {
if index < 0 || index >= len(e.pages) {
return PageCapabilities{}, fmt.Errorf("page index %d out of range", index)
}
return PageCapabilities{Insert: e.sourceRGB(), Copy: true, Delete: true, Move: true, Resize: true}, nil
}
// sourceRGB 判断新建RGB对象能否直接使用文档默认颜色空间 // sourceRGB 判断新建RGB对象能否直接使用文档默认颜色空间
// 返回: bool 是否为8位RGB文档 // 返回: bool 是否为8位RGB文档
func (e *Editor) sourceRGB() bool { func (e *Editor) sourceRGB() bool {
@@ -484,19 +518,6 @@ func (e *Editor) loadSourcePage(index int) error {
return nil return nil
} }
// ObjectCapabilities 获取对象可执行的操作,不支持的内容保持原文,不强制转换
// 字体缺失或子集不包含原文时仍可保真移动、复制和删除,不自动重新排版
// 显式提供可用字体后可通过UpdateObject替换;原文布局未知时不自动推断段落选项
// 入参: page 页面索引, id 对象标识
// 返回: ObjectCapabilities 操作能力, error 错误信息
func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, error) {
layer, index, err := e.findObject(page, id)
if err != nil {
return ObjectCapabilities{}, err
}
return e.objectCapabilities(layer.Objects[index], make(map[*editorXML]bool)), nil
}
// objectCapabilities 检查对象能力,在同次遍历中复用原容器排序检查 // objectCapabilities 检查对象能力,在同次遍历中复用原容器排序检查
// 入参: object 当前对象, orderable 原容器排序能力 // 入参: object 当前对象, orderable 原容器排序能力
// 返回: ObjectCapabilities 操作能力 // 返回: ObjectCapabilities 操作能力
@@ -822,27 +843,6 @@ func (e *Editor) editorFont(id string) (FontMetrics, error) {
return metrics, nil return metrics, nil
} }
// FontData 获取编辑使用的字体数据,外部字体不写入文档资源
// 入参: id 字体资源标识
// 返回: []byte 独立字体数据, error 错误信息
func (e *Editor) FontData(id string) ([]byte, error) {
for _, resource := range e.resources {
if resource.font != nil && resource.font.ID == id && len(resource.data) != 0 {
return bytes.Clone(resource.data), nil
}
}
if e.source != nil {
if definition := e.source.reader.fontCache[id]; definition != nil && definition.FontFile != "" {
return e.source.reader.FontData(id)
}
}
sfnt, err := e.editorFont(id)
if err != nil {
return nil, err
}
return sfnt.Write(), nil
}
// editorTextMeasurable 判断原字形能否在不使用回退字体的情况下度量 // editorTextMeasurable 判断原字形能否在不使用回退字体的情况下度量
// 入参: text 原文字对象 // 入参: text 原文字对象
// 返回: bool 是否可准确度量 // 返回: bool 是否可准确度量
+165 -165
View File
@@ -207,6 +207,171 @@ func (e *Editor) AddDrawParam(draw DrawParam) (string, error) {
return e.addEditorDrawParam(cloneEditorData(draw)) return e.addEditorDrawParam(cloneEditorData(draw))
} }
// StyleObjects 原子更新对象透明度、文字颜色及路径填充和描边样式
// 入参: page 页面索引, ids 对象标识, style 待修改属性
// 返回: error 错误信息
func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
objects, _, err := e.selectedObjects(page, ids)
if err != nil {
return err
}
for i := range objects {
if style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil {
capability, err := e.ObjectCapabilities(page, ids[i])
if err != nil {
return err
}
if !capability.Paint {
return fmt.Errorf("object %q cannot be painted: %w", ids[i], capability.editError())
}
}
objects[i], err = e.styleObject(objects[i], style)
if err != nil {
return err
}
}
return e.updateObjects(page, objects, true)
}
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作
// 文字复制字体、字号、颜色及透明度,沿用目标段落设置;字体标识须属于当前文档
// 路径按页面中的实际线宽和虚线长度复制,保持目标变换
// 入参: page 目标页面索引, ids 目标对象标识, source 样式来源快照
// 返回: error 错误信息
func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) (err error) {
if err := e.validateCopyStyle(source); err != nil {
return err
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil || len(objects) == 0 {
return err
}
for _, object := range objects {
if object.Type != source.Type {
return fmt.Errorf("style requires objects of the same type")
}
}
count, maximum := len(e.resources), e.maxID
ready := e.source != nil && e.source.idsReady
defer func() {
if err != nil {
clear(e.resources[count:])
e.resources, e.maxID = e.resources[:count], maximum
if e.source != nil {
e.source.idsReady = ready
}
}
}()
if source.Type == "TextObject" && source.TextObject.FillColor == nil || source.Type == "PathObject" && (source.PathObject.FillColor == nil || source.PathObject.StrokeColor == nil) {
black, err := e.RGBColor(color.NRGBA{A: 255})
if err != nil {
return err
}
if source.Type == "TextObject" {
source.TextObject.FillColor = black
} else {
if source.PathObject.FillColor == nil {
source.PathObject.FillColor = black
}
if source.PathObject.StrokeColor == nil {
source.PathObject.StrokeColor = (*StrokeColor)(black)
}
}
}
if source.Type == "TextObject" {
objects, err = e.styleTextObjects(objects, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size})
if err != nil {
return err
}
for i := range objects {
objects[i].TextObject.FillColor = cloneEditorData(source.TextObject.FillColor)
objects[i].TextObject.Alpha = cloneEditorData(source.TextObject.Alpha)
}
return e.UpdateObjects(page, objects)
}
for i, object := range objects {
object = cloneEditorData(object)
switch source.Type {
case "PathObject":
object.PathObject = copyEditorPathStyle(object.PathObject, source.PathObject, editorStrokeScale(source.PathObject.CTM)/editorStrokeScale(object.PathObject.CTM))
case "ImageObject":
object.ImageObject.Alpha = source.ImageObject.Alpha
}
objects[i] = object
}
return e.updateObjects(page, objects, true)
}
// StyleImageBorders 原子修改图片边框,保留未修改的颜色、线型及原文扩展
// 入参: page 页面索引, ids 图片标识, style 边框增量
// 返回: error 错误信息
func (e *Editor) StyleImageBorders(page int, ids []string, style ImageBorderStyle) error {
for _, value := range []*float64{style.LineWidth, style.HorizontalRadius, style.VerticalRadius} {
if value != nil && (!finite(*value) || *value < 0) {
return fmt.Errorf("border dimensions must be finite and nonnegative")
}
}
if style.Color != nil {
if err := e.editorColor((*FillColor)(style.Color)); err != nil {
return err
}
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil {
return err
}
for i := range objects {
if objects[i].Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", ids[i])
}
if style.Enabled != nil && !*style.Enabled {
objects[i].ImageObject.Border = nil
continue
}
border := cloneEditorData(objects[i].ImageObject.Border)
if border == nil {
if style == (ImageBorderStyle{}) {
continue
}
border = &ImageBorder{}
}
if style.LineWidth != nil {
border.LineWidth = cloneEditorData(style.LineWidth)
}
if style.HorizontalRadius != nil {
border.HorizonalCornerRadius = *style.HorizontalRadius
}
if style.VerticalRadius != nil {
border.VerticalCornerRadius = *style.VerticalRadius
}
if style.Color != nil {
border.BorderColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.Color), (*FillColor)(border.BorderColor)))
}
objects[i].ImageObject.Border = border
}
return e.updateObjects(page, objects, true)
}
// SetImageBorders 原子替换图片边框,nil移除边框
// 入参: page 页面索引, ids 图片标识, border 边框样式
// 返回: error 错误信息
func (e *Editor) SetImageBorders(page int, ids []string, border *ImageBorder) error {
if err := e.validateImageBorder(border); err != nil {
return err
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil {
return err
}
for i := range objects {
if objects[i].Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", ids[i])
}
objects[i].ImageObject.Border = cloneEditorData(border)
}
return e.updateObjects(page, objects, true)
}
// editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照 // editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照
// 入参: id 绘制参数标识, visited 已访问标识 // 入参: id 绘制参数标识, visited 已访问标识
// 返回: *DrawParam 合并参数, error 错误信息 // 返回: *DrawParam 合并参数, error 错误信息
@@ -316,32 +481,6 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
return cloneEditorObject(object) return cloneEditorObject(object)
} }
// StyleObjects 原子更新对象透明度、文字颜色及路径填充和描边样式
// 入参: page 页面索引, ids 对象标识, style 待修改属性
// 返回: error 错误信息
func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
objects, _, err := e.selectedObjects(page, ids)
if err != nil {
return err
}
for i := range objects {
if style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil {
capability, err := e.ObjectCapabilities(page, ids[i])
if err != nil {
return err
}
if !capability.Paint {
return fmt.Errorf("object %q cannot be painted: %w", ids[i], capability.editError())
}
}
objects[i], err = e.styleObject(objects[i], style)
if err != nil {
return err
}
}
return e.updateObjects(page, objects, true)
}
// styleObject 仅替换请求的外观字段,保留内容、定位及未修改样式 // styleObject 仅替换请求的外观字段,保留内容、定位及未修改样式
// 入参: object 原对象, style 待修改属性 // 入参: object 原对象, style 待修改属性
// 返回: GraphicObject 修改后的独立对象, error 错误信息 // 返回: GraphicObject 修改后的独立对象, error 错误信息
@@ -439,75 +578,6 @@ func editorStyleColor(color, before *FillColor) *FillColor {
return result return result
} }
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作
// 文字复制字体、字号、颜色及透明度,沿用目标段落设置;字体标识须属于当前文档
// 路径按页面中的实际线宽和虚线长度复制,保持目标变换
// 入参: page 目标页面索引, ids 目标对象标识, source 样式来源快照
// 返回: error 错误信息
func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) (err error) {
if err := e.validateCopyStyle(source); err != nil {
return err
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil || len(objects) == 0 {
return err
}
for _, object := range objects {
if object.Type != source.Type {
return fmt.Errorf("style requires objects of the same type")
}
}
count, maximum := len(e.resources), e.maxID
ready := e.source != nil && e.source.idsReady
defer func() {
if err != nil {
clear(e.resources[count:])
e.resources, e.maxID = e.resources[:count], maximum
if e.source != nil {
e.source.idsReady = ready
}
}
}()
if source.Type == "TextObject" && source.TextObject.FillColor == nil || source.Type == "PathObject" && (source.PathObject.FillColor == nil || source.PathObject.StrokeColor == nil) {
black, err := e.RGBColor(color.NRGBA{A: 255})
if err != nil {
return err
}
if source.Type == "TextObject" {
source.TextObject.FillColor = black
} else {
if source.PathObject.FillColor == nil {
source.PathObject.FillColor = black
}
if source.PathObject.StrokeColor == nil {
source.PathObject.StrokeColor = (*StrokeColor)(black)
}
}
}
if source.Type == "TextObject" {
objects, err = e.styleTextObjects(objects, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size})
if err != nil {
return err
}
for i := range objects {
objects[i].TextObject.FillColor = cloneEditorData(source.TextObject.FillColor)
objects[i].TextObject.Alpha = cloneEditorData(source.TextObject.Alpha)
}
return e.UpdateObjects(page, objects)
}
for i, object := range objects {
object = cloneEditorData(object)
switch source.Type {
case "PathObject":
object.PathObject = copyEditorPathStyle(object.PathObject, source.PathObject, editorStrokeScale(source.PathObject.CTM)/editorStrokeScale(object.PathObject.CTM))
case "ImageObject":
object.ImageObject.Alpha = source.ImageObject.Alpha
}
objects[i] = object
}
return e.updateObjects(page, objects, true)
}
// validateCopyStyle 校验样式来源,不分配资源或改写对象 // validateCopyStyle 校验样式来源,不分配资源或改写对象
// 入参: source 有效外观快照 // 入参: source 有效外观快照
// 返回: error 错误信息 // 返回: error 错误信息
@@ -588,76 +658,6 @@ func editorStrokeScale(ctm string) float64 {
return 1 return 1
} }
// StyleImageBorders 原子修改图片边框,保留未修改的颜色、线型及原文扩展
// 入参: page 页面索引, ids 图片标识, style 边框增量
// 返回: error 错误信息
func (e *Editor) StyleImageBorders(page int, ids []string, style ImageBorderStyle) error {
for _, value := range []*float64{style.LineWidth, style.HorizontalRadius, style.VerticalRadius} {
if value != nil && (!finite(*value) || *value < 0) {
return fmt.Errorf("border dimensions must be finite and nonnegative")
}
}
if style.Color != nil {
if err := e.editorColor((*FillColor)(style.Color)); err != nil {
return err
}
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil {
return err
}
for i := range objects {
if objects[i].Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", ids[i])
}
if style.Enabled != nil && !*style.Enabled {
objects[i].ImageObject.Border = nil
continue
}
border := cloneEditorData(objects[i].ImageObject.Border)
if border == nil {
if style == (ImageBorderStyle{}) {
continue
}
border = &ImageBorder{}
}
if style.LineWidth != nil {
border.LineWidth = cloneEditorData(style.LineWidth)
}
if style.HorizontalRadius != nil {
border.HorizonalCornerRadius = *style.HorizontalRadius
}
if style.VerticalRadius != nil {
border.VerticalCornerRadius = *style.VerticalRadius
}
if style.Color != nil {
border.BorderColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.Color), (*FillColor)(border.BorderColor)))
}
objects[i].ImageObject.Border = border
}
return e.updateObjects(page, objects, true)
}
// SetImageBorders 原子替换图片边框,nil移除边框
// 入参: page 页面索引, ids 图片标识, border 边框样式
// 返回: error 错误信息
func (e *Editor) SetImageBorders(page int, ids []string, border *ImageBorder) error {
if err := e.validateImageBorder(border); err != nil {
return err
}
objects, _, err := e.selectedObjects(page, ids)
if err != nil {
return err
}
for i := range objects {
if objects[i].Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", ids[i])
}
objects[i].ImageObject.Border = cloneEditorData(border)
}
return e.updateObjects(page, objects, true)
}
// validateImageBorder 校验图片边框的线型、圆角和绘制颜色 // validateImageBorder 校验图片边框的线型、圆角和绘制颜色
// 入参: border 边框 // 入参: border 边框
// 返回: error 错误信息 // 返回: error 错误信息
+44 -44
View File
@@ -32,34 +32,6 @@ type MissingGlyphError struct {
Characters string `json:"characters"` Characters string `json:"characters"`
} }
// Error 返回缺失字符的Unicode码点
// 返回: string 错误描述
func (e *MissingGlyphError) Error() string {
var codes []string
for _, char := range e.Characters {
codes = append(codes, fmt.Sprintf("U+%04X", char))
}
return fmt.Sprintf("font %q does not contain %s", e.FontID, strings.Join(codes, ", "))
}
// missingGlyphError 汇总缺失字符,无缺字时返回nil
// 入参: id 字体资源标识, characters 缺失字符
// 返回: error 缺字诊断
func missingGlyphError(id string, characters []rune) error {
if len(characters) == 0 {
return nil
}
seen := make(map[rune]bool)
var value strings.Builder
for _, char := range characters {
if !seen[char] {
seen[char] = true
value.WriteRune(char)
}
}
return &MissingGlyphError{FontID: id, Characters: value.String()}
}
// TextLayout 本地横向段落选项 // TextLayout 本地横向段落选项
// Shape显式启用FontShaper,保存为标准TextCode和CGTransform,不改变阅读时的原文定位 // Shape显式启用FontShaper,保存为标准TextCode和CGTransform,不改变阅读时的原文定位
// Shaping提供可比较的塑形选项,非零值也会启用塑形,并随会话内文字编辑保留 // Shaping提供可比较的塑形选项,非零值也会启用塑形,并随会话内文字编辑保留
@@ -89,6 +61,22 @@ type TextStyle struct {
Color string Color string
} }
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染
type textLayout struct {
value string
options TextLayout
}
// Error 返回缺失字符的Unicode码点
// 返回: string 错误描述
func (e *MissingGlyphError) Error() string {
var codes []string
for _, char := range e.Characters {
codes = append(codes, fmt.Sprintf("U+%04X", char))
}
return fmt.Sprintf("font %q does not contain %s", e.FontID, strings.Join(codes, ", "))
}
// StyleText 原子更新同页文字样式,全部校验通过后提交一次撤销记录 // StyleText 原子更新同页文字样式,全部校验通过后提交一次撤销记录
// 已知段落重新排版;原布局未知时替换字体和颜色保留坐标,不允许自动调整字号 // 已知段落重新排版;原布局未知时替换字体和颜色保留坐标,不允许自动调整字号
// 入参: page 页面索引, ids 文字对象标识, style 样式增量 // 入参: page 页面索引, ids 文字对象标识, style 样式增量
@@ -287,6 +275,34 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
return nil return nil
} }
// TextLayout 获取最近一次排版的原文与选项,区分软换行和显式换行
// 信息仅在当前编辑过程保留,读取OFD时返回定位后的文字和默认选项
// 返回: string 原文, TextLayout 排版选项
func (obj TextObject) TextLayout() (string, TextLayout) {
if obj.layout != nil {
return obj.layout.value, obj.layout.options
}
return obj.Text(), TextLayout{}
}
// missingGlyphError 汇总缺失字符,无缺字时返回nil
// 入参: id 字体资源标识, characters 缺失字符
// 返回: error 缺字诊断
func missingGlyphError(id string, characters []rune) error {
if len(characters) == 0 {
return nil
}
seen := make(map[rune]bool)
var value strings.Builder
for _, char := range characters {
if !seen[char] {
seen[char] = true
value.WriteRune(char)
}
}
return &MissingGlyphError{FontID: id, Characters: value.String()}
}
// rewriteTextDeltas 局部替换字符步进,保留未修改字符及末尾附加位移 // rewriteTextDeltas 局部替换字符步进,保留未修改字符及末尾附加位移
// 入参: value 原位移, old 原字符, replacement 替换字符, start 起点, end 终点, advance 字体步进 // 入参: value 原位移, old 原字符, replacement 替换字符, start 起点, end 终点, advance 字体步进
// 返回: string 更新后的位移数组 // 返回: string 更新后的位移数组
@@ -372,22 +388,6 @@ func (e *Editor) styleTextObjects(objects []GraphicObject, style TextStyle) ([]G
return updates, nil return updates, nil
} }
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染
type textLayout struct {
value string
options TextLayout
}
// TextLayout 获取最近一次排版的原文与选项,区分软换行和显式换行
// 信息仅在当前编辑过程保留,读取OFD时返回定位后的文字和默认选项
// 返回: string 原文, TextLayout 排版选项
func (obj TextObject) TextLayout() (string, TextLayout) {
if obj.layout != nil {
return obj.layout.value, obj.layout.options
}
return obj.Text(), TextLayout{}
}
// breakTextLines 优先使用Unicode断行机会,过长词仅在字素边界折行,不丢弃空白 // breakTextLines 优先使用Unicode断行机会,过长词仅在字素边界折行,不丢弃空白
// 入参: runes 段落字符, advances 含字距的字符步进, width 本地排版宽度, wrap 是否自动折行, spacing 附加字距, indent 首行缩进 // 入参: runes 段落字符, advances 含字距的字符步进, width 本地排版宽度, wrap 是否自动折行, spacing 附加字距, indent 首行缩进
// 返回: [][2]int 各行的字符起止索引,左闭右开 // 返回: [][2]int 各行的字符起止索引,左闭右开
+100 -100
View File
@@ -119,6 +119,106 @@ func (e *Editor) ResizeObjects(page int, ids []string, box Box) error {
}) })
} }
// ImageBounds 返回完整图片经Boundary位移及CTM变换后的轴对齐边界,不应用裁剪或父级变换
// 返回: Box 所在坐标系中的完整图片边界, error 错误信息
func (obj ImageObject) ImageBounds() (Box, error) {
box, err := creationBox(obj.Boundary)
if err != nil {
return Box{}, err
}
if obj.CTM == "" {
return box, nil
}
if _, err := creationNumbers(obj.CTM, 6); err != nil {
return Box{}, err
}
return TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(obj.CTM)).TransformBox(Box{W: 1, H: 1}), nil
}
// CropImage 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域
// 使用ImageObject.ImageBounds返回的范围可还原完整图片;提交一次撤销记录
// 入参: page 页面索引, id 图片对象标识, box 页面毫米坐标中的保留范围
// 返回: error 错误信息
func (e *Editor) CropImage(page int, id string, box Box) error {
object, err := e.Object(page, id)
if err != nil {
return err
}
if object.Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", id)
}
image := &object.ImageObject
if err := cropImageObject(image, box); err != nil {
return err
}
object.state.crop = nil
return e.UpdateObject(page, id, object)
}
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留直角旋转及镜像方向
// 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局
// 入参: page 页面索引, id 图片对象标识, mode 为contain(完整显示)或cover(填满裁剪)
// 返回: error 错误信息
func (e *Editor) FitImage(page int, id, mode string) error {
object, err := e.Object(page, id)
if err != nil {
return err
}
if object.Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", id)
}
if err := e.fitImage(&object.ImageObject, mode); err != nil {
return err
}
object.state.crop = nil
return e.UpdateObject(page, id, object)
}
// TextFrame 将对象Boundary逆变换到本地坐标并返回轴对齐边界
// 直角旋转与翻转不改变本地段落宽度
// 返回: Box 对象本地坐标中的边界, error 错误信息
func (obj TextObject) TextFrame() (Box, error) {
box, err := creationBox(obj.Boundary)
if err != nil {
return Box{}, err
}
if obj.CTM != "" {
if _, err := creationNumbers(obj.CTM, 6); err != nil {
return Box{}, err
}
}
m, ok := NewMatrix(obj.CTM).Invert()
if !ok {
return Box{}, fmt.Errorf("text transform is not invertible")
}
return m.TransformBox(Box{W: box.W, H: box.H}), nil
}
// ResizeTextFrame 调整文字本地坐标的左侧偏移及宽度,保留页面方向;重新排版后通过UpdateObject提交
// 支持轴向缩放、直角旋转与镜像,不支持斜切或其他角度旋转
// 入参: offset 本地左侧位移, width 新的本地排版宽度,单位为毫米
// 返回: TextObject 调整后的文字对象, error 错误信息
func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error) {
frame, err := obj.TextFrame()
if err != nil {
return TextObject{}, err
}
if !axisAlignedMatrix(NewMatrix(obj.CTM)) {
return TextObject{}, fmt.Errorf("text frame resizing requires an axis-aligned transform")
}
if !finite(offset) || !finite(width) || width <= 0 {
return TextObject{}, fmt.Errorf("text frame requires a finite offset and a positive width")
}
boundary, _ := ParseBox(obj.Boundary)
m := TranslationMatrix(boundary.X, boundary.Y).Multiply(NewMatrix(obj.CTM))
frame.X += offset
frame.W = width
box := m.TransformBox(frame)
obj.CTM = TranslationMatrix(-box.X, -box.Y).Multiply(m).Multiply(TranslationMatrix(offset, 0)).String()
obj.Boundary = editorBoxString(box)
return obj, nil
}
// transformObject 对原有复杂对象仅更新变换,不重建其文字、画刷或其他局部属性 // transformObject 对原有复杂对象仅更新变换,不重建其文字、画刷或其他局部属性
// 入参: object 对象, dx、dy 页面位移, scale 缩放比例 // 入参: object 对象, dx、dy 页面位移, scale 缩放比例
// 返回: GraphicObject 新对象, error 错误信息 // 返回: GraphicObject 新对象, error 错误信息
@@ -345,42 +445,6 @@ func transformObjectClips(clips *Clips, matrix Matrix) *Clips {
return &result return &result
} }
// ImageBounds 返回完整图片经Boundary位移及CTM变换后的轴对齐边界,不应用裁剪或父级变换
// 返回: Box 所在坐标系中的完整图片边界, error 错误信息
func (obj ImageObject) ImageBounds() (Box, error) {
box, err := creationBox(obj.Boundary)
if err != nil {
return Box{}, err
}
if obj.CTM == "" {
return box, nil
}
if _, err := creationNumbers(obj.CTM, 6); err != nil {
return Box{}, err
}
return TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(obj.CTM)).TransformBox(Box{W: 1, H: 1}), nil
}
// CropImage 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域
// 使用ImageObject.ImageBounds返回的范围可还原完整图片;提交一次撤销记录
// 入参: page 页面索引, id 图片对象标识, box 页面毫米坐标中的保留范围
// 返回: error 错误信息
func (e *Editor) CropImage(page int, id string, box Box) error {
object, err := e.Object(page, id)
if err != nil {
return err
}
if object.Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", id)
}
image := &object.ImageObject
if err := cropImageObject(image, box); err != nil {
return err
}
object.state.crop = nil
return e.UpdateObject(page, id, object)
}
// cropImageObject 更新图片副本的裁剪和局部坐标,不修改资源 // cropImageObject 更新图片副本的裁剪和局部坐标,不修改资源
// 入参: image 图片对象副本, box 页面毫米坐标中的保留范围 // 入参: image 图片对象副本, box 页面毫米坐标中的保留范围
// 返回: error 错误信息 // 返回: error 错误信息
@@ -420,25 +484,6 @@ func cropImageObject(image *ImageObject, box Box) error {
return nil return nil
} }
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留直角旋转及镜像方向
// 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局
// 入参: page 页面索引, id 图片对象标识, mode 为contain(完整显示)或cover(填满裁剪)
// 返回: error 错误信息
func (e *Editor) FitImage(page int, id, mode string) error {
object, err := e.Object(page, id)
if err != nil {
return err
}
if object.Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", id)
}
if err := e.fitImage(&object.ImageObject, mode); err != nil {
return err
}
object.state.crop = nil
return e.UpdateObject(page, id, object)
}
// fitImage 将原始像素比例应用到当前图片框,适应时保留空白,填充时裁掉超出部分 // fitImage 将原始像素比例应用到当前图片框,适应时保留空白,填充时裁掉超出部分
// 入参: obj 图片对象副本, mode 为contain或cover // 入参: obj 图片对象副本, mode 为contain或cover
// 返回: error 错误信息 // 返回: error 错误信息
@@ -484,51 +529,6 @@ func (e *Editor) fitImage(obj *ImageObject, mode string) error {
return nil return nil
} }
// TextFrame 将对象Boundary逆变换到本地坐标并返回轴对齐边界
// 直角旋转与翻转不改变本地段落宽度
// 返回: Box 对象本地坐标中的边界, error 错误信息
func (obj TextObject) TextFrame() (Box, error) {
box, err := creationBox(obj.Boundary)
if err != nil {
return Box{}, err
}
if obj.CTM != "" {
if _, err := creationNumbers(obj.CTM, 6); err != nil {
return Box{}, err
}
}
m, ok := NewMatrix(obj.CTM).Invert()
if !ok {
return Box{}, fmt.Errorf("text transform is not invertible")
}
return m.TransformBox(Box{W: box.W, H: box.H}), nil
}
// ResizeTextFrame 调整文字本地坐标的左侧偏移及宽度,保留页面方向;重新排版后通过UpdateObject提交
// 支持轴向缩放、直角旋转与镜像,不支持斜切或其他角度旋转
// 入参: offset 本地左侧位移, width 新的本地排版宽度,单位为毫米
// 返回: TextObject 调整后的文字对象, error 错误信息
func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error) {
frame, err := obj.TextFrame()
if err != nil {
return TextObject{}, err
}
if !axisAlignedMatrix(NewMatrix(obj.CTM)) {
return TextObject{}, fmt.Errorf("text frame resizing requires an axis-aligned transform")
}
if !finite(offset) || !finite(width) || width <= 0 {
return TextObject{}, fmt.Errorf("text frame requires a finite offset and a positive width")
}
boundary, _ := ParseBox(obj.Boundary)
m := TranslationMatrix(boundary.X, boundary.Y).Multiply(NewMatrix(obj.CTM))
frame.X += offset
frame.W = width
box := m.TransformBox(frame)
obj.CTM = TranslationMatrix(-box.X, -box.Y).Multiply(m).Multiply(TranslationMatrix(offset, 0)).String()
obj.Boundary = editorBoxString(box)
return obj, nil
}
// editorBoxString 使用统一精度序列化创作边界 // editorBoxString 使用统一精度序列化创作边界
// 入参: box 矩形范围 // 入参: box 矩形范围
// 返回: string 标准Boundary属性值 // 返回: string 标准Boundary属性值
+8 -8
View File
@@ -20,14 +20,6 @@ import (
"github.com/tdewolff/font" "github.com/tdewolff/font"
) )
// fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和
// 入参: sfnt 已解析的字体
// 返回: []byte 独立SFNT数据
func fontSFNTData(sfnt *font.SFNT) []byte {
data, _ := serializeOTF(sfnt.Tables)
return data
}
// FixFontDataAggressive 激进修复字体数据 // FixFontDataAggressive 激进修复字体数据
// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据 // 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据
// 对显式字形映射补充私有字符cmap,便于按glyph id渲染 // 对显式字形映射补充私有字符cmap,便于按glyph id渲染
@@ -59,6 +51,14 @@ func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, ma
return fixed, newData, mapping, mc, err return fixed, newData, mapping, mc, err
} }
// fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和
// 入参: sfnt 已解析的字体
// 返回: []byte 独立SFNT数据
func fontSFNTData(sfnt *font.SFNT) []byte {
data, _ := serializeOTF(sfnt.Tables)
return data
}
// isBareCFFData 检查是否为CFF裸字体数据 // isBareCFFData 检查是否为CFF裸字体数据
// 入参: data 字体数据 // 入参: data 字体数据
// 返回: bool 是否为CFF裸数据 // 返回: bool 是否为CFF裸数据
+23 -23
View File
@@ -123,29 +123,6 @@ type resolvedFontResult struct {
err error err error
} }
// resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态
func (r *Renderer) resetFontCache() {
r.fontSourcesCache = newFontSourceCache()
r.resetFontBackendCache()
}
// resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存
func (r *Renderer) resetFontBackendCache() {
r.preparedFonts = make(map[string]*PreparedFont)
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
if r.fontSourcesCache != nil {
r.fontSourcesCache.fallback = ResolvedFont{}
r.fontSourcesCache.fallbackRead = false
}
r.fontMetrics = renderCache[[32]byte, FontMetrics]{limit: 32 << 20}
r.glyphOutlines = renderCache[glyphOutlineKey, GeometryPath]{limit: 16 << 20}
for _, state := range r.backendStates {
if fonts, ok := state.(interface{ resetFonts() }); ok {
fonts.resetFonts()
}
}
}
// ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别 // ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别
// 入参: id 字体资源标识, exact 是否要求精确匹配 // 入参: id 字体资源标识, exact 是否要求精确匹配
// 返回: ResolvedFont 字体与来源, error 解析错误 // 返回: ResolvedFont 字体与来源, error 解析错误
@@ -198,3 +175,26 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) {
r.preparedFonts[id] = prepared r.preparedFonts[id] = prepared
return prepared, nil return prepared, nil
} }
// resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态
func (r *Renderer) resetFontCache() {
r.fontSourcesCache = newFontSourceCache()
r.resetFontBackendCache()
}
// resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存
func (r *Renderer) resetFontBackendCache() {
r.preparedFonts = make(map[string]*PreparedFont)
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
if r.fontSourcesCache != nil {
r.fontSourcesCache.fallback = ResolvedFont{}
r.fontSourcesCache.fallbackRead = false
}
r.fontMetrics = renderCache[[32]byte, FontMetrics]{limit: 32 << 20}
r.glyphOutlines = renderCache[glyphOutlineKey, GeometryPath]{limit: 16 << 20}
for _, state := range r.backendStates {
if fonts, ok := state.(interface{ resetFonts() }); ok {
fonts.resetFonts()
}
}
}
+15 -15
View File
@@ -26,6 +26,21 @@ import (
"golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/simplifiedchinese"
) )
// cffStandardStrings CFF标准字符串表
var cffStandardStrings = []string{
".notdef", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quoteright", "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "quoteleft", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "exclamdown", "cent", "sterling", "fraction", "yen", "florin", "section", "currency", "quotesingle", "quotedblleft", "quotedblright", "guillemotleft", "guillemotright", "dagger", "daggerdbl", "fi", "fl", "endash", "emdash", "paragraph", "bullet", "quotesinglbase", "quotedblbase", "second", "circumflex", "breve", "dotaccent", "dieresis", "grave", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "emspace",
"AE", "ordfeminine", "Lslash", "Oslash", "OE", "ordmasculine", "ae", "dotlessi", "lslash", "oslash", "oe", "germandbls", "onesuperior", "logicalnot", "mu", "trademark", "Eth", "onehalf", "plusminus", "Thorn", "onequarter", "divide", "brokenbar", "degree", "thorn", "threequarters", "twosuperior", "registered", "minus", "eth", "multiply", "threesuperior", "copyright", "Aacute", "Acircumflex", "Adieresis", "Agrave", "Aring", "Atilde", "Ccedilla", "Eacute", "Ecircumflex", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Ntilde", "Oacute", "Ocircumflex", "Odieresis", "Ograve", "Otilde", "Scaron", "Uacute", "Ucircumflex", "Udieresis", "Ugrave", "Yacute", "Ydieresis", "Zcaron", "aacute", "acircumflex", "adieresis", "agrave", "aring", "atilde", "ccedilla", "eacute", "ecircumflex", "edieresis", "egrave", "iacute", "icircumflex", "idieresis", "igrave", "ntilde", "oacute", "ocircumflex", "odieresis", "ograve", "otilde", "scaron", "uacute", "ucircumflex", "udieresis", "ugrave", "yacute", "ydieresis", "zcaron", "exclamsmall", "Hungarumlautsmall", "dollaroldstyle", "dollarsuperior", "ampersandsmall", "Acutesmall", "parenleftsuperior", "parenrightsuperior", "2dotlead", "nbspace", "1dotlead", "zerooldstyle", "oneoldstyle", "twooldstyle", "threeoldstyle", "fouroldstyle", "fiveoldstyle", "sixoldstyle", "sevenoldstyle", "eightoldstyle", "nineoldstyle", "commasuperior", "threequartersemdash", "periodsuperior", "questionsmall", "asuperior", "bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior", "lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior", "tsuperior", "ff", "ffi", "ffl", "parenleftinferior", "parenrightinferior", "Circumflexsmall", "hyphensuperior", "Gravesmall", "Asmall", "Bsmall", "Csmall", "Dsmall", "Esmall", "Fsmall", "Gsmall", "Hsmall", "Ismall", "Jsmall", "Ksmall", "Lsmall", "Msmall", "Nsmall", "Osmall", "Psmall", "Qsmall", "Rsmall", "Ssmall", "Tsmall", "Usmall", "Vsmall", "Wsmall", "Xsmall", "Ysmall", "Zsmall", "colonmonetary", "onefitted", "rupiah", "Tildesmall", "exclamdownsmall", "centoldstyle", "Lslashsmall", "Scaronsmall", "Zcaronsmall", "Dieresissmall", "Brevesmall", "Caronsmall", "Dotaccentsmall", "Macronsmall", "figuredash", "hypheninferior", "Ogoneksmall", "Ringsmall", "Cedillasmall", "questiondownsmall", "oneeighth", "threeeighths", "fiveeighths", "seveneighths", "onethird", "twothirds", "zerosuperior", "foursuperior", "fivesuperior", "sixsuperior", "sevensuperior", "eightsuperior", "ninesuperior", "zeroinferior", "oneinferior", "twoinferior", "threeinferior", "fourinferior", "fiveinferior", "sixinferior", "seveninferior", "eightinferior", "nineinferior", "centinferior", "dollarinferior", "periodinferior", "commainferior", "Agravesmall", "Aacutesmall", "Acircumflexsmall", "Atildesmall", "Adieresissmall", "Aringsmall", "AEsmall", "Ccedillasmall", "Egravesmall", "Eacutesmall", "Ecircumflexsmall", "Edieresissmall", "Igravesmall", "Iacutesmall", "Icircumflexsmall", "Idieresissmall", "Ethsmall", "Ntildesmall", "Ogravesmall", "Oacutesmall", "Ocircumflexsmall", "Otildesmall", "Odieresissmall", "OEsmall", "Oslashsmall", "Ugravesmall", "Uacutesmall", "Ucircumflexsmall", "Udieresissmall", "Yacutesmall", "Thornsmall", "Ydieresissmall", "001.000", "001.001", "001.002", "001.003", "Black", "Bold", "Book", "Light", "Medium", "Regular", "Roman", "Semibold",
}
// cffDict 使用float64存储所有数值,以统一处理整数和实数
type cffDict map[int][]float64
// type2Operand Type2操作数
type type2Operand struct {
value int
outStart int
}
// wrapCFFToOTF 将CFF裸数据包装为OpenType字体格式 // wrapCFFToOTF 将CFF裸数据包装为OpenType字体格式
// 入参: cffData CFF字体数据 // 入参: cffData CFF字体数据
// 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息 // 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息
@@ -80,9 +95,6 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
return data, mapping, err return data, mapping, err
} }
// cffDict 使用float64存储所有数值,以统一处理整数和实数
type cffDict map[int][]float64
// normalizeCFFCharstrings 规范化Type2提示指令的等价编码,保留轮廓和提示数据 // normalizeCFFCharstrings 规范化Type2提示指令的等价编码,保留轮廓和提示数据
// 入参: data CFF字体数据 // 入参: data CFF字体数据
// 返回: []byte 标准化后的CFF数据, error 错误信息 // 返回: []byte 标准化后的CFF数据, error 错误信息
@@ -945,12 +957,6 @@ func readCFFLocalSubrs(data []byte, fdArrOff int, fdCount int) ([][][]byte, []cf
return localSubrs, privateDicts return localSubrs, privateDicts
} }
// type2Operand Type2操作数
type type2Operand struct {
value int
outStart int
}
// inlineType2CharString 内联Type2 CharString子程序调用 // inlineType2CharString 内联Type2 CharString子程序调用
// 入参: data CharString数据, localSubrs 本地子程序, globalSubrs 全局子程序, depth 递归深度 // 入参: data CharString数据, localSubrs 本地子程序, globalSubrs 全局子程序, depth 递归深度
// 返回: []byte 内联后的CharString数据 // 返回: []byte 内联后的CharString数据
@@ -1719,9 +1725,3 @@ func getUnicodeFromName(name string) rune {
} }
return 0 return 0
} }
// cffStandardStrings CFF标准字符串表
var cffStandardStrings = []string{
".notdef", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quoteright", "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "quoteleft", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "exclamdown", "cent", "sterling", "fraction", "yen", "florin", "section", "currency", "quotesingle", "quotedblleft", "quotedblright", "guillemotleft", "guillemotright", "dagger", "daggerdbl", "fi", "fl", "endash", "emdash", "paragraph", "bullet", "quotesinglbase", "quotedblbase", "second", "circumflex", "breve", "dotaccent", "dieresis", "grave", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "emspace",
"AE", "ordfeminine", "Lslash", "Oslash", "OE", "ordmasculine", "ae", "dotlessi", "lslash", "oslash", "oe", "germandbls", "onesuperior", "logicalnot", "mu", "trademark", "Eth", "onehalf", "plusminus", "Thorn", "onequarter", "divide", "brokenbar", "degree", "thorn", "threequarters", "twosuperior", "registered", "minus", "eth", "multiply", "threesuperior", "copyright", "Aacute", "Acircumflex", "Adieresis", "Agrave", "Aring", "Atilde", "Ccedilla", "Eacute", "Ecircumflex", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Ntilde", "Oacute", "Ocircumflex", "Odieresis", "Ograve", "Otilde", "Scaron", "Uacute", "Ucircumflex", "Udieresis", "Ugrave", "Yacute", "Ydieresis", "Zcaron", "aacute", "acircumflex", "adieresis", "agrave", "aring", "atilde", "ccedilla", "eacute", "ecircumflex", "edieresis", "egrave", "iacute", "icircumflex", "idieresis", "igrave", "ntilde", "oacute", "ocircumflex", "odieresis", "ograve", "otilde", "scaron", "uacute", "ucircumflex", "udieresis", "ugrave", "yacute", "ydieresis", "zcaron", "exclamsmall", "Hungarumlautsmall", "dollaroldstyle", "dollarsuperior", "ampersandsmall", "Acutesmall", "parenleftsuperior", "parenrightsuperior", "2dotlead", "nbspace", "1dotlead", "zerooldstyle", "oneoldstyle", "twooldstyle", "threeoldstyle", "fouroldstyle", "fiveoldstyle", "sixoldstyle", "sevenoldstyle", "eightoldstyle", "nineoldstyle", "commasuperior", "threequartersemdash", "periodsuperior", "questionsmall", "asuperior", "bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior", "lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior", "tsuperior", "ff", "ffi", "ffl", "parenleftinferior", "parenrightinferior", "Circumflexsmall", "hyphensuperior", "Gravesmall", "Asmall", "Bsmall", "Csmall", "Dsmall", "Esmall", "Fsmall", "Gsmall", "Hsmall", "Ismall", "Jsmall", "Ksmall", "Lsmall", "Msmall", "Nsmall", "Osmall", "Psmall", "Qsmall", "Rsmall", "Ssmall", "Tsmall", "Usmall", "Vsmall", "Wsmall", "Xsmall", "Ysmall", "Zsmall", "colonmonetary", "onefitted", "rupiah", "Tildesmall", "exclamdownsmall", "centoldstyle", "Lslashsmall", "Scaronsmall", "Zcaronsmall", "Dieresissmall", "Brevesmall", "Caronsmall", "Dotaccentsmall", "Macronsmall", "figuredash", "hypheninferior", "Ogoneksmall", "Ringsmall", "Cedillasmall", "questiondownsmall", "oneeighth", "threeeighths", "fiveeighths", "seveneighths", "onethird", "twothirds", "zerosuperior", "foursuperior", "fivesuperior", "sixsuperior", "sevensuperior", "eightsuperior", "ninesuperior", "zeroinferior", "oneinferior", "twoinferior", "threeinferior", "fourinferior", "fiveinferior", "sixinferior", "seveninferior", "eightinferior", "nineinferior", "centinferior", "dollarinferior", "periodinferior", "commainferior", "Agravesmall", "Aacutesmall", "Acircumflexsmall", "Atildesmall", "Adieresissmall", "Aringsmall", "AEsmall", "Ccedillasmall", "Egravesmall", "Eacutesmall", "Ecircumflexsmall", "Edieresissmall", "Igravesmall", "Iacutesmall", "Icircumflexsmall", "Idieresissmall", "Ethsmall", "Ntildesmall", "Ogravesmall", "Oacutesmall", "Ocircumflexsmall", "Otildesmall", "Odieresissmall", "OEsmall", "Oslashsmall", "Ugravesmall", "Uacutesmall", "Ucircumflexsmall", "Udieresissmall", "Yacutesmall", "Thornsmall", "Ydieresissmall", "001.000", "001.001", "001.002", "001.003", "Black", "Bold", "Book", "Light", "Medium", "Regular", "Roman", "Semibold",
}
+391
View File
@@ -18,8 +18,17 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"sort"
) )
// cmapSegment cmap表段结构
// 字段: start 开始字符, end 结束字符, delta 增量, offset 偏移
type cmapSegment struct {
start, end uint16
delta int16
offset uint16
}
// normalizeCmap 转换旧式双字节子表,保留各编码记录及原字形编号 // normalizeCmap 转换旧式双字节子表,保留各编码记录及原字形编号
// 入参: data 字符映射表 // 入参: data 字符映射表
// 返回: []byte 等价字符映射表, error 子表错误 // 返回: []byte 等价字符映射表, error 子表错误
@@ -120,3 +129,385 @@ func parseCmapFormat2(data []byte) (map[rune]uint16, error) {
} }
return mapping, nil return mapping, nil
} }
// packedGlyphRune 获取包装字体字符
// 入参: gid 字形ID
// 返回: rune 包装字体字符
func packedGlyphRune(gid uint16) rune {
return 0xF0000 + rune(gid)
}
// parseCmapMappings 解析cmap字符映射
// 入参: data cmap表数据
// 返回: map[rune]uint16 字符到字形映射
func parseCmapMappings(data []byte) map[rune]uint16 {
if len(data) < 4 {
return nil
}
numTables := int(binary.BigEndian.Uint16(data[2:4]))
result := make(map[rune]uint16)
for i := 0; i < numTables; i++ {
pos := 4 + i*8
if pos+8 > len(data) {
break
}
platformID := binary.BigEndian.Uint16(data[pos : pos+2])
if platformID != 0 && platformID != 3 {
continue
}
offset := int(binary.BigEndian.Uint32(data[pos+4 : pos+8]))
if offset+2 > len(data) {
continue
}
switch binary.BigEndian.Uint16(data[offset : offset+2]) {
case 0:
parseCmapFormat0(data[offset:], result)
case 4:
parseCmapFormat4(data[offset:], result)
case 6:
parseCmapFormat6(data[offset:], result)
case 12:
parseCmapFormat12(data[offset:], result)
}
}
if len(result) == 0 {
return nil
}
return result
}
// parseCmapFormat0 解析cmap format 0
// 入参: data 子表数据, result 字符映射
func parseCmapFormat0(data []byte, result map[rune]uint16) {
if len(data) < 262 {
return
}
for i := 0; i < 256; i++ {
gid := uint16(data[6+i])
if gid != 0 {
result[rune(i)] = gid
}
}
}
// parseCmapFormat4 解析cmap format 4
// 入参: data 子表数据, result 字符映射
func parseCmapFormat4(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := int(binary.BigEndian.Uint16(data[2:4]))
if length > len(data) {
length = len(data)
}
segCount := int(binary.BigEndian.Uint16(data[6:8]) / 2)
endPos := 14
startPos := endPos + segCount*2 + 2
deltaPos := startPos + segCount*2
rangePos := deltaPos + segCount*2
if rangePos+segCount*2 > length {
return
}
for i := 0; i < segCount; i++ {
end := binary.BigEndian.Uint16(data[endPos+i*2:])
start := binary.BigEndian.Uint16(data[startPos+i*2:])
delta := int16(binary.BigEndian.Uint16(data[deltaPos+i*2:]))
rangeOffsetPos := rangePos + i*2
rangeOffset := binary.BigEndian.Uint16(data[rangeOffsetPos:])
if start == 0xFFFF && end == 0xFFFF {
continue
}
for c := start; c <= end; c++ {
var gid uint16
if rangeOffset == 0 {
gid = uint16(int(c) + int(delta))
} else {
gidPos := rangeOffsetPos + int(rangeOffset) + int(c-start)*2
if gidPos+2 > length {
continue
}
gid = binary.BigEndian.Uint16(data[gidPos:])
if gid != 0 {
gid = uint16(int(gid) + int(delta))
}
}
if gid != 0 {
result[rune(c)] = gid
}
if c == 0xFFFF {
break
}
}
}
}
// parseCmapFormat6 解析cmap format 6
// 入参: data 子表数据, result 字符映射
func parseCmapFormat6(data []byte, result map[rune]uint16) {
if len(data) < 10 {
return
}
firstCode := int(binary.BigEndian.Uint16(data[6:8]))
entryCount := int(binary.BigEndian.Uint16(data[8:10]))
for i := 0; i < entryCount && 10+i*2+2 <= len(data); i++ {
gid := binary.BigEndian.Uint16(data[10+i*2:])
if gid != 0 {
result[rune(firstCode+i)] = gid
}
}
}
// parseCmapFormat12 解析cmap format 12
// 入参: data 子表数据, result 字符映射
func parseCmapFormat12(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := int(binary.BigEndian.Uint32(data[4:8]))
if length > len(data) {
length = len(data)
}
nGroups := int(binary.BigEndian.Uint32(data[12:16]))
for i := 0; i < nGroups; i++ {
pos := 16 + i*12
if pos+12 > length {
break
}
startChar := binary.BigEndian.Uint32(data[pos:])
endChar := binary.BigEndian.Uint32(data[pos+4:])
startGID := binary.BigEndian.Uint32(data[pos+8:])
for c := startChar; c <= endChar; c++ {
gid := startGID + c - startChar
if gid != 0 && gid <= 0xFFFF {
result[rune(c)] = uint16(gid)
}
if c == endChar {
break
}
}
}
}
// addPackedGlyphMapping 添加字形ID私有映射
// 入参: mapping 字符映射, numGlyphs 字形数量
func addPackedGlyphMapping(mapping map[rune]uint16, numGlyphs uint16) {
for i := uint16(0); i < numGlyphs; i++ {
mapping[packedGlyphRune(i)] = i
}
}
// buildCmapTable 构建cmap表 (Format 4)
// 入参: numGlyphs 字形数量, mapping 字符映射
// 返回: []byte cmap表数据
func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
if shouldBuildCmapFormat12(mapping) {
return buildCmapTableFormat12(numGlyphs, mapping)
}
var segs []cmapSegment
if mapping == nil {
end := uint16(0xFFFF)
if numGlyphs > 0 {
end = numGlyphs - 1
}
segs = append(segs, cmapSegment{start: 0, end: end, delta: 0, offset: 0})
} else {
var codes []int
for r := range mapping {
if r <= 0xFFFF {
codes = append(codes, int(r))
}
}
sort.Ints(codes)
if len(codes) > 0 {
start := codes[0]
prev := start
for i := 1; i < len(codes); i++ {
curr := codes[i]
if curr != prev+1 {
segs = append(segs, cmapSegment{start: uint16(start), end: uint16(prev), delta: 0, offset: 0})
start = curr
}
prev = curr
}
segs = append(segs, cmapSegment{start: uint16(start), end: uint16(prev), delta: 0, offset: 0})
}
}
segs = append(segs, cmapSegment{start: 0xFFFF, end: 0xFFFF, delta: 1, offset: 0})
segCount := uint16(len(segs))
searchRange := uint16(1)
entrySelector := uint16(0)
for searchRange*2 <= segCount {
searchRange *= 2
entrySelector++
}
searchRange *= 2
rangeShift := segCount*2 - searchRange
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint16(4))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(segCount*2))
binary.Write(buf, binary.BigEndian, searchRange)
binary.Write(buf, binary.BigEndian, entrySelector)
binary.Write(buf, binary.BigEndian, rangeShift)
var endCounts, startCounts, idDeltas, idRangeOffsets []uint16
var glyphIds []uint16
for _, s := range segs {
endCounts = append(endCounts, s.end)
startCounts = append(startCounts, s.start)
if mapping == nil {
idDeltas = append(idDeltas, 0)
idRangeOffsets = append(idRangeOffsets, 0)
continue
}
if s.start == 0xFFFF {
idDeltas = append(idDeltas, 1)
idRangeOffsets = append(idRangeOffsets, 0)
continue
}
idDeltas = append(idDeltas, 0)
currentGlyphIdx := len(glyphIds)
for c := int(s.start); c <= int(s.end); c++ {
gid := mapping[rune(c)]
glyphIds = append(glyphIds, gid)
}
offset := (int(segCount)-int(len(idRangeOffsets))-1)*2 + 2 + currentGlyphIdx*2
idRangeOffsets = append(idRangeOffsets, uint16(offset))
}
for _, v := range endCounts {
binary.Write(buf, binary.BigEndian, v)
}
binary.Write(buf, binary.BigEndian, uint16(0))
for _, v := range startCounts {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range idDeltas {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range idRangeOffsets {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range glyphIds {
binary.Write(buf, binary.BigEndian, v)
}
data := buf.Bytes()
binary.BigEndian.PutUint16(data[2:4], uint16(len(data)))
mainBuf := new(bytes.Buffer)
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(1))
binary.Write(mainBuf, binary.BigEndian, uint16(3))
binary.Write(mainBuf, binary.BigEndian, uint16(1))
binary.Write(mainBuf, binary.BigEndian, uint32(12))
mainBuf.Write(data)
return mainBuf.Bytes()
}
// shouldBuildCmapFormat12 判断终止码、补充平面字符或format 4长度溢出时是否需要format 12
// 入参: mapping 字符到字形的映射
// 返回: bool 是否需要构建format 12子表
func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
if mapping == nil {
return false
}
var codes []int
for r := range mapping {
if r >= 0xFFFF {
return true
}
codes = append(codes, int(r))
}
if len(codes) == 0 {
return false
}
sort.Ints(codes)
segCount := 1
glyphIDCount := 1
prev := codes[0]
for i := 1; i < len(codes); i++ {
curr := codes[i]
if curr == prev {
continue
}
if curr != prev+1 {
segCount++
}
glyphIDCount++
prev = curr
}
segCount++
length := 16 + segCount*8 + glyphIDCount*2
return length > 0xFFFF
}
// buildCmapTableFormat12 构建cmap表 (Format 12)
// 入参: numGlyphs 字形数量, mapping 字符映射
// 返回: []byte cmap表数据
func buildCmapTableFormat12(numGlyphs uint16, mapping map[rune]uint16) []byte {
var codes []int
if mapping == nil {
for i := 0; i < int(numGlyphs); i++ {
codes = append(codes, i)
}
} else {
for r := range mapping {
if r >= 0 {
codes = append(codes, int(r))
}
}
}
sort.Ints(codes)
type cmapGroup struct {
startChar uint32
endChar uint32
startGID uint32
}
var groups []cmapGroup
for i := 0; i < len(codes); {
r := rune(codes[i])
gid := uint16(codes[i])
if mapping != nil {
gid = mapping[r]
}
group := cmapGroup{startChar: uint32(r), endChar: uint32(r), startGID: uint32(gid)}
prevRune := r
prevGID := gid
i++
for i < len(codes) {
nextRune := rune(codes[i])
nextGID := uint16(codes[i])
if mapping != nil {
nextGID = mapping[nextRune]
}
if nextRune != prevRune+1 || nextGID != prevGID+1 {
break
}
group.endChar = uint32(nextRune)
prevRune = nextRune
prevGID = nextGID
i++
}
groups = append(groups, group)
}
sub := new(bytes.Buffer)
binary.Write(sub, binary.BigEndian, uint16(12))
binary.Write(sub, binary.BigEndian, uint16(0))
binary.Write(sub, binary.BigEndian, uint32(16+12*len(groups)))
binary.Write(sub, binary.BigEndian, uint32(0))
binary.Write(sub, binary.BigEndian, uint32(len(groups)))
for _, group := range groups {
binary.Write(sub, binary.BigEndian, group.startChar)
binary.Write(sub, binary.BigEndian, group.endChar)
binary.Write(sub, binary.BigEndian, group.startGID)
}
mainBuf := new(bytes.Buffer)
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(2))
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(4))
binary.Write(mainBuf, binary.BigEndian, uint32(20))
binary.Write(mainBuf, binary.BigEndian, uint16(3))
binary.Write(mainBuf, binary.BigEndian, uint16(10))
binary.Write(mainBuf, binary.BigEndian, uint32(20))
mainBuf.Write(sub.Bytes())
return mainBuf.Bytes()
}
+20 -20
View File
@@ -60,26 +60,6 @@ func (f FontFile) Faces() ([]FontFace, error) {
return faces, nil return faces, nil
} }
// fontFaceInfo 从名称表读取字体信息
// 入参: data 名称表数据, index 零起始字体索引
// 返回: FontFace 字体信息
func fontFaceInfo(data []byte, index int) FontFace {
records, values := fontNameValues(data)
name := func(ids ...uint16) string {
for _, id := range ids {
for _, record := range records {
if record.Name == id {
if value := strings.TrimSpace(values[[4]uint16{record.Platform, record.Encoding, record.Language, id}]); value != "" {
return value
}
}
}
}
return ""
}
return FontFace{Index: index, FullName: name(4, 6, 16, 1), Family: name(16, 1), Style: name(17, 2), PostScriptName: name(6), Names: fontNamesFromTable(data)}
}
// Face 提取指定字体,返回独立的OpenType数据,可用于预览或AddFont,且不修改原文件 // Face 提取指定字体,返回独立的OpenType数据,可用于预览或AddFont,且不修改原文件
// 入参: index 零起始字体索引,非集合文件只能为0 // 入参: index 零起始字体索引,非集合文件只能为0
// 返回: []byte 独立字体数据, error 格式或索引错误 // 返回: []byte 独立字体数据, error 格式或索引错误
@@ -139,6 +119,26 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) {
return missing.String(), nil return missing.String(), nil
} }
// fontFaceInfo 从名称表读取字体信息
// 入参: data 名称表数据, index 零起始字体索引
// 返回: FontFace 字体信息
func fontFaceInfo(data []byte, index int) FontFace {
records, values := fontNameValues(data)
name := func(ids ...uint16) string {
for _, id := range ids {
for _, record := range records {
if record.Name == id {
if value := strings.TrimSpace(values[[4]uint16{record.Platform, record.Encoding, record.Language, id}]); value != "" {
return value
}
}
}
}
return ""
}
return FontFace{Index: index, FullName: name(4, 6, 16, 1), Family: name(16, 1), Style: name(17, 2), PostScriptName: name(6), Names: fontNamesFromTable(data)}
}
// fontFileCount 校验字体或集合头并读取字体数量 // fontFileCount 校验字体或集合头并读取字体数量
// 入参: data OpenType数据 // 入参: data OpenType数据
// 返回: int 字体数量, error 格式错误 // 返回: int 字体数量, error 格式错误
+188 -183
View File
@@ -25,6 +25,18 @@ import (
"time" "time"
) )
var _ fs.FS = (*FontFS)(nil)
var _ fs.ReadDirFS = (*FontFS)(nil)
var _ fs.GlobFS = (*FontFS)(nil)
var _ fs.File = (*fontMemFile)(nil)
var _ fs.ReadDirFile = (*fontDir)(nil)
var _ fs.DirEntry = (*fontDirEntry)(nil)
// FontFile 内存字体文件 // FontFile 内存字体文件
type FontFile struct { type FontFile struct {
Name string Name string
@@ -39,6 +51,67 @@ type FontFS struct {
candidates []fontFileCandidate 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
}
// fontMemFile 内存字体文件
type fontMemFile struct {
*bytes.Reader
info fontFileInfo
}
// fontDir 字体目录
type fontDir struct {
offset int
entries []fs.DirEntry
}
// fontDirEntry 字体目录条目
type fontDirEntry struct {
info fontFileInfo
}
// fontFileInfo 字体文件信息
type fontFileInfo struct {
name string
size int64
mode fs.FileMode
}
// NewFontFS 创建内存字体文件系统 // NewFontFS 创建内存字体文件系统
// 入参: fonts 字体文件列表 // 入参: fonts 字体文件列表
// 返回: *FontFS 内存字体文件系统 // 返回: *FontFS 内存字体文件系统
@@ -217,6 +290,121 @@ func (r *Reader) MatchFontFiles(files []string, ids ...string) ([]string, error)
return names, nil return names, nil
} }
// Stat 获取字体文件信息
// 返回: fs.FileInfo 文件信息, error 错误信息
func (f *fontMemFile) Stat() (fs.FileInfo, error) {
return f.info, nil
}
// Close 关闭字体文件
// 返回: error 错误信息
func (f *fontMemFile) Close() error {
return nil
}
// Stat 获取字体目录信息
// 返回: fs.FileInfo 文件信息, error 错误信息
func (d *fontDir) Stat() (fs.FileInfo, error) {
return fontFileInfo{name: ".", mode: fs.ModeDir}, nil
}
// Read 字体目录不提供字节内容,始终返回EOF
// 入参: []byte 读取缓冲区,不使用
// 返回: int 始终为0, error 始终为io.EOF
func (d *fontDir) Read([]byte) (int, error) {
return 0, io.EOF
}
// Close 关闭字体目录
// 返回: error 错误信息
func (d *fontDir) Close() error {
return nil
}
// ReadDir 读取字体目录条目
// 入参: count 读取数量
// 返回: []fs.DirEntry 目录条目, error 错误信息
func (d *fontDir) ReadDir(count int) ([]fs.DirEntry, error) {
if d.offset >= len(d.entries) {
if count <= 0 {
return nil, nil
}
return nil, io.EOF
}
if count <= 0 || d.offset+count > len(d.entries) {
count = len(d.entries) - d.offset
}
entries := d.entries[d.offset : d.offset+count]
d.offset += count
return entries, nil
}
// Name 获取目录条目名称
// 返回: string 目录条目名称
func (e fontDirEntry) Name() string {
return e.info.Name()
}
// IsDir 判断是否为目录
// 返回: bool 是否为目录
func (e fontDirEntry) IsDir() bool {
return false
}
// Type 获取目录条目类型
// 返回: fs.FileMode 文件模式
func (e fontDirEntry) Type() fs.FileMode {
return e.info.Mode().Type()
}
// Info 获取目录条目信息
// 返回: fs.FileInfo 文件信息, error 错误信息
func (e fontDirEntry) Info() (fs.FileInfo, error) {
return e.info, nil
}
// Name 获取文件名
// 返回: string 文件名
func (i fontFileInfo) Name() string {
if i.name == "" {
return "."
}
return i.name
}
// Size 获取文件大小
// 返回: int64 文件大小
func (i fontFileInfo) Size() int64 {
return i.size
}
// Mode 获取文件模式
// 返回: fs.FileMode 文件模式
func (i fontFileInfo) Mode() fs.FileMode {
if i.mode != 0 {
return i.mode
}
return 0444
}
// ModTime 获取文件修改时间
// 返回: time.Time 修改时间
func (i fontFileInfo) ModTime() time.Time {
return time.Time{}
}
// IsDir 判断是否为目录
// 返回: bool 是否为目录
func (i fontFileInfo) IsDir() bool {
return i.mode.IsDir()
}
// Sys 获取底层文件信息
// 返回: any 底层文件信息
func (i fontFileInfo) Sys() any {
return nil
}
// matchStyle 匹配指定样式的字体名称 // matchStyle 匹配指定样式的字体名称
// 入参: names 字体名称列表, bold 是否粗体, italic 是否斜体 // 入参: names 字体名称列表, bold 是否粗体, italic 是否斜体
// 返回: []fontFileMatch 字体匹配列表 // 返回: []fontFileMatch 字体匹配列表
@@ -315,18 +503,6 @@ func cleanFontName(name string) string {
return strings.ToLower(name) return strings.ToLower(name)
} }
// fontFileCandidate 字体文件候选
type fontFileCandidate struct {
name string
face int
base string
lowerBase string
filename string
normalized string
names []string
fold string
}
// fontFileCandidates 筛选字体文件候选 // fontFileCandidates 筛选字体文件候选
// 入参: names 文件名列表, base 文件名提取函数 // 入参: names 文件名列表, base 文件名提取函数
// 返回: []fontFileCandidate 字体文件候选列表 // 返回: []fontFileCandidate 字体文件候选列表
@@ -349,15 +525,6 @@ func fontFileCandidates(names []string, base func(string) string) []fontFileCand
return files return files
} }
// fontPatternMatcher 字体文件匹配器
type fontPatternMatcher struct {
pattern string
lowerPattern string
stem string
aliases []string
priority int
}
// newFontPatternMatcher 创建字体文件匹配器 // newFontPatternMatcher 创建字体文件匹配器
// 入参: pattern 匹配模式 // 入参: pattern 匹配模式
// 返回: fontPatternMatcher 字体文件匹配器 // 返回: fontPatternMatcher 字体文件匹配器
@@ -483,22 +650,6 @@ func (m fontPatternMatcher) styleSuffixNormalized(name string) string {
return "" return ""
} }
// fontFileMatch 字体文件匹配结果
type fontFileMatch struct {
name string
face int
priority int
rank int
styleRank int
sortName string
}
// fontFileKey 字体文件与集合索引的去重键
type fontFileKey struct {
name string
face int
}
// appendFontFileMatch 追加字体文件匹配结果 // appendFontFileMatch 追加字体文件匹配结果
// 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体 // 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
func appendFontFileMatch(matches *[]fontFileMatch, seen map[fontFileKey]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) { func appendFontFileMatch(matches *[]fontFileMatch, seen map[fontFileKey]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) {
@@ -632,149 +783,3 @@ func fontFileKnownStyleSuffix(suffix string) bool {
return false return false
} }
} }
// fontMemFile 内存字体文件
type fontMemFile struct {
*bytes.Reader
info fontFileInfo
}
// Stat 获取字体文件信息
// 返回: fs.FileInfo 文件信息, error 错误信息
func (f *fontMemFile) Stat() (fs.FileInfo, error) {
return f.info, nil
}
// Close 关闭字体文件
// 返回: error 错误信息
func (f *fontMemFile) Close() error {
return nil
}
// fontDir 字体目录
type fontDir struct {
offset int
entries []fs.DirEntry
}
// Stat 获取字体目录信息
// 返回: fs.FileInfo 文件信息, error 错误信息
func (d *fontDir) Stat() (fs.FileInfo, error) {
return fontFileInfo{name: ".", mode: fs.ModeDir}, nil
}
// Read 字体目录不提供字节内容,始终返回EOF
// 入参: []byte 读取缓冲区,不使用
// 返回: int 始终为0, error 始终为io.EOF
func (d *fontDir) Read([]byte) (int, error) {
return 0, io.EOF
}
// Close 关闭字体目录
// 返回: error 错误信息
func (d *fontDir) Close() error {
return nil
}
// ReadDir 读取字体目录条目
// 入参: count 读取数量
// 返回: []fs.DirEntry 目录条目, error 错误信息
func (d *fontDir) ReadDir(count int) ([]fs.DirEntry, error) {
if d.offset >= len(d.entries) {
if count <= 0 {
return nil, nil
}
return nil, io.EOF
}
if count <= 0 || d.offset+count > len(d.entries) {
count = len(d.entries) - d.offset
}
entries := d.entries[d.offset : d.offset+count]
d.offset += count
return entries, nil
}
// fontDirEntry 字体目录条目
type fontDirEntry struct {
info fontFileInfo
}
// Name 获取目录条目名称
// 返回: string 目录条目名称
func (e fontDirEntry) Name() string {
return e.info.Name()
}
// IsDir 判断是否为目录
// 返回: bool 是否为目录
func (e fontDirEntry) IsDir() bool {
return false
}
// Type 获取目录条目类型
// 返回: fs.FileMode 文件模式
func (e fontDirEntry) Type() fs.FileMode {
return e.info.Mode().Type()
}
// Info 获取目录条目信息
// 返回: fs.FileInfo 文件信息, error 错误信息
func (e fontDirEntry) Info() (fs.FileInfo, error) {
return e.info, nil
}
// fontFileInfo 字体文件信息
type fontFileInfo struct {
name string
size int64
mode fs.FileMode
}
// Name 获取文件名
// 返回: string 文件名
func (i fontFileInfo) Name() string {
if i.name == "" {
return "."
}
return i.name
}
// Size 获取文件大小
// 返回: int64 文件大小
func (i fontFileInfo) Size() int64 {
return i.size
}
// Mode 获取文件模式
// 返回: fs.FileMode 文件模式
func (i fontFileInfo) Mode() fs.FileMode {
if i.mode != 0 {
return i.mode
}
return 0444
}
// ModTime 获取文件修改时间
// 返回: time.Time 修改时间
func (i fontFileInfo) ModTime() time.Time {
return time.Time{}
}
// IsDir 判断是否为目录
// 返回: bool 是否为目录
func (i fontFileInfo) IsDir() bool {
return i.mode.IsDir()
}
// Sys 获取底层文件信息
// 返回: any 底层文件信息
func (i fontFileInfo) Sys() any {
return nil
}
var _ fs.FS = (*FontFS)(nil)
var _ fs.ReadDirFS = (*FontFS)(nil)
var _ fs.GlobFS = (*FontFS)(nil)
var _ fs.File = (*fontMemFile)(nil)
var _ fs.ReadDirFile = (*fontDir)(nil)
var _ fs.DirEntry = (*fontDirEntry)(nil)
+76 -76
View File
@@ -52,6 +52,20 @@ type FontInfo struct {
Used int `json:"used"` Used int `json:"used"`
} }
// FontInfoScanner 逐页统计字体用量,不保留页面图元,扫描期间不得修改阅读器或渲染器配置
type FontInfoScanner struct {
renderer *Renderer
doc *Document
index int
usage map[string]int
}
// fontUsagePage 逐图元统计字体的页面解码器
type fontUsagePage struct {
renderer *Renderer
usage map[string]int
}
// Fonts 获取OFD声明的字体列表 // Fonts 获取OFD声明的字体列表
// 返回: []Font 字体列表, error 错误信息 // 返回: []Font 字体列表, error 错误信息
func (r *Reader) Fonts() ([]Font, error) { func (r *Reader) Fonts() ([]Font, error) {
@@ -104,48 +118,6 @@ func (r *Reader) FontData(id string) ([]byte, error) {
return data, nil return data, nil
} }
// embeddedFontFace 按资源缓存内嵌字体名称并返回独立副本,不解析字形轮廓
// 入参: of OFD字体定义
// 返回: *FontFace 字体信息, error 资源读取错误
func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) {
name := r.ResPath(of.FontFile)
faces, ok := r.fontFaces[name]
if !ok {
data, err := r.readFile(name)
if err != nil {
return nil, err
}
if data, err = font.ToSFNT(data); err == nil {
if count, err := fontFileCount(data); err == nil {
faces = make([]*FontFace, count)
for index := range faces {
if tables, err := fontFileTables(data, index); err == nil {
info := fontFaceInfo(tables["name"], index)
faces[index] = &info
}
}
}
}
r.fontFaces[name] = faces
}
if len(faces) == 0 {
return nil, nil
}
names := make([][]string, len(faces))
for index, face := range faces {
if face != nil {
names[index] = face.Names
}
}
index := fontNameIndex(names, []string{of.FontName, of.FamilyName}, of.Bold, of.Italic)
if faces[index] == nil {
return nil, nil
}
face := *faces[index]
face.Names = slices.Clone(face.Names)
return &face, nil
}
// FontInfos 获取OFD字体诊断信息 // FontInfos 获取OFD字体诊断信息
// 返回: []FontInfo 字体诊断列表, error 错误信息 // 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) FontInfos() ([]FontInfo, error) { func (r *Renderer) FontInfos() ([]FontInfo, error) {
@@ -158,14 +130,6 @@ func (r *Renderer) FontInfos() ([]FontInfo, error) {
return scanner.Infos() return scanner.Infos()
} }
// FontInfoScanner 逐页统计字体用量,不保留页面图元,扫描期间不得修改阅读器或渲染器配置
type FontInfoScanner struct {
renderer *Renderer
doc *Document
index int
usage map[string]int
}
// ScanFontInfos 创建分步字体诊断,页面读取失败的处理与FontInfos一致 // ScanFontInfos 创建分步字体诊断,页面读取失败的处理与FontInfos一致
// 返回: *FontInfoScanner 字体诊断扫描器, error 错误信息 // 返回: *FontInfoScanner 字体诊断扫描器, error 错误信息
func (r *Renderer) ScanFontInfos() (*FontInfoScanner, error) { func (r *Renderer) ScanFontInfos() (*FontInfoScanner, error) {
@@ -226,6 +190,68 @@ func (r *Renderer) FontInfosFromPages(pages []*PageContent) ([]FontInfo, error)
return r.fontInfos(r.fontUsage(doc, pages)) return r.fontInfos(r.fontUsage(doc, pages))
} }
// UnmarshalXML 解析页面内容中的图层字体
// 入参: d XML解码器, start 页面起始节点
// 返回: error 错误信息
func (p *fontUsagePage) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
if start.Name.Local != "Page" {
return fmt.Errorf("expected element type <Page> but have <%s>", start.Name.Local)
}
return decodeObjectContainer(d, start, func(d *xml.Decoder, node xml.StartElement) error {
if node.Name.Local != "Content" {
return d.Skip()
}
return decodeObjectContainer(d, node, func(d *xml.Decoder, layer xml.StartElement) error {
if layer.Name.Local != "Layer" {
return d.Skip()
}
return p.decodeObjects(d, layer, p.renderer.drawParamDefaults(attrValue(layer, "DrawParam"), nil))
})
})
}
// embeddedFontFace 按资源缓存内嵌字体名称并返回独立副本,不解析字形轮廓
// 入参: of OFD字体定义
// 返回: *FontFace 字体信息, error 资源读取错误
func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) {
name := r.ResPath(of.FontFile)
faces, ok := r.fontFaces[name]
if !ok {
data, err := r.readFile(name)
if err != nil {
return nil, err
}
if data, err = font.ToSFNT(data); err == nil {
if count, err := fontFileCount(data); err == nil {
faces = make([]*FontFace, count)
for index := range faces {
if tables, err := fontFileTables(data, index); err == nil {
info := fontFaceInfo(tables["name"], index)
faces[index] = &info
}
}
}
}
r.fontFaces[name] = faces
}
if len(faces) == 0 {
return nil, nil
}
names := make([][]string, len(faces))
for index, face := range faces {
if face != nil {
names[index] = face.Names
}
}
index := fontNameIndex(names, []string{of.FontName, of.FamilyName}, of.Bold, of.Italic)
if faces[index] == nil {
return nil, nil
}
face := *faces[index]
face.Names = slices.Clone(face.Names)
return &face, nil
}
// fontInfoDocument 获取字体诊断文档结构 // fontInfoDocument 获取字体诊断文档结构
// 返回: *Document 文档结构, error 错误信息 // 返回: *Document 文档结构, error 错误信息
func (r *Renderer) fontInfoDocument() (*Document, error) { func (r *Renderer) fontInfoDocument() (*Document, error) {
@@ -352,12 +378,6 @@ func (r *Renderer) fontUsage(doc *Document, pages []*PageContent) map[string]int
return usage return usage
} }
// fontUsagePage 逐图元统计字体的页面解码器
type fontUsagePage struct {
renderer *Renderer
usage map[string]int
}
// readPageFontUsage 读取页面字体用量,不保留页面图元 // readPageFontUsage 读取页面字体用量,不保留页面图元
// 入参: page 页面引用 // 入参: page 页面引用
// 返回: map[string]int 字体使用次数, error 错误信息 // 返回: map[string]int 字体使用次数, error 错误信息
@@ -379,26 +399,6 @@ func (r *Renderer) readPageFontUsage(page Page) (map[string]int, error) {
return content.usage, nil return content.usage, nil
} }
// UnmarshalXML 解析页面内容中的图层字体
// 入参: d XML解码器, start 页面起始节点
// 返回: error 错误信息
func (p *fontUsagePage) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
if start.Name.Local != "Page" {
return fmt.Errorf("expected element type <Page> but have <%s>", start.Name.Local)
}
return decodeObjectContainer(d, start, func(d *xml.Decoder, node xml.StartElement) error {
if node.Name.Local != "Content" {
return d.Skip()
}
return decodeObjectContainer(d, node, func(d *xml.Decoder, layer xml.StartElement) error {
if layer.Name.Local != "Layer" {
return d.Skip()
}
return p.decodeObjects(d, layer, p.renderer.drawParamDefaults(attrValue(layer, "DrawParam"), nil))
})
})
}
// decodeObjects 逐个读取图元并复用字体统计规则 // decodeObjects 逐个读取图元并复用字体统计规则
// 入参: d XML解码器, start 容器起始节点, defaults 默认绘制参数 // 入参: d XML解码器, start 容器起始节点, defaults 默认绘制参数
// 返回: error 错误信息 // 返回: error 错误信息
+25 -25
View File
@@ -20,15 +20,6 @@ import (
"strings" "strings"
) )
// fontMatchRule 字体匹配规则
type fontMatchRule struct {
Keys []string
Names []string
Files []string
System string
NoSyntheticBold bool
}
const ( const (
fontMatchNone = iota fontMatchNone = iota
fontMatchExact fontMatchExact
@@ -114,22 +105,13 @@ var fontMatchRules = normalizeFontMatchRules([]fontMatchRule{
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "") var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
// normalizeFontMatchRules 预处理固定字体规则的匹配名称,保留候选名称及顺序 // fontMatchRule 字体匹配规则
// 入参: rules 字体匹配规则 type fontMatchRule struct {
// 返回: []fontMatchRule 预处理后的规则 Keys []string
func normalizeFontMatchRules(rules []fontMatchRule) []fontMatchRule { Names []string
for i := range rules { Files []string
rule := &rules[i] System string
keys := append(append(rule.Keys, rule.Names...), rule.System) NoSyntheticBold bool
rule.Keys = nil
for _, key := range keys {
key = fontNormalizeName(key)
if key != "" && !slices.Contains(rule.Keys, key) {
rule.Keys = append(rule.Keys, key)
}
}
}
return rules
} }
// FontNormalizeName 规范化字体名称 // FontNormalizeName 规范化字体名称
@@ -160,6 +142,24 @@ func FontSystemNames(names ...string) []string {
return fontSystemNames(names...) 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 = fontNormalizeName(key)
if key != "" && !slices.Contains(rule.Keys, key) {
rule.Keys = append(rule.Keys, key)
}
}
}
return rules
}
// fontNormalizeName 规范化字体名称 // fontNormalizeName 规范化字体名称
// 入参: name 字体名称 // 入参: name 字体名称
// 返回: string 规范化后的字体名称 // 返回: string 规范化后的字体名称
+3 -3
View File
@@ -44,6 +44,9 @@ type fontShaper struct {
shapeBuffer *harfbuzz.Buffer shapeBuffer *harfbuzz.Buffer
} }
// shapingFace 固定双向排版的字体来源,不隐式替换字体
type shapingFace struct{ face *typefont.Face }
// ShapeText 使用默认选项塑形单行文字,不改动原文定位 // ShapeText 使用默认选项塑形单行文字,不改动原文定位
// 入参: value 单行原文, size 毫米字号 // 入参: value 单行原文, size 毫米字号
// 返回: []ShapedGlyph 定位字形, error 塑形错误 // 返回: []ShapedGlyph 定位字形, error 塑形错误
@@ -152,9 +155,6 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
return result, nil return result, nil
} }
// shapingFace 固定双向排版的字体来源,不隐式替换字体
type shapingFace struct{ face *typefont.Face }
// ResolveFace 返回指定字体 // ResolveFace 返回指定字体
// 入参: char 字符 // 入参: char 字符
// 返回: *typefont.Face 字体 // 返回: *typefont.Face 字体
+9 -9
View File
@@ -29,15 +29,21 @@ import (
"github.com/tdewolff/font" "github.com/tdewolff/font"
) )
// fontSourceKind 字体来源类型
type fontSourceKind uint8
const ( const (
fontSourceFile fontSourceKind = iota fontSourceFile fontSourceKind = iota
fontSourceFS fontSourceFS
fontSourceSystem fontSourceSystem
) )
// systemFontIndex 串行初始化系统字体索引,不注册绘图后端
var systemFontIndex struct {
sync.Mutex
fonts *font.SystemFonts
}
// fontSourceKind 字体来源类型
type fontSourceKind uint8
// fontSource 字体来源 // fontSource 字体来源
type fontSource struct { type fontSource struct {
kind fontSourceKind kind fontSourceKind
@@ -78,12 +84,6 @@ 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)} return &fontSourceCache{candidates: make(map[string][]fontSource), directories: make(map[string][]fontFileCandidate), filesystems: make(map[int][]fontFileCandidate), data: make(map[fontSourceKey]fontSourceData)}
} }
// systemFontIndex 串行初始化系统字体索引,不注册绘图后端
var systemFontIndex struct {
sync.Mutex
fonts *font.SystemFonts
}
// readFontSource 读取并拆出独立字体,保留集合中的名称和样式选择 // readFontSource 读取并拆出独立字体,保留集合中的名称和样式选择
// 入参: source 字体来源, definition 字体定义 // 入参: source 字体来源, definition 字体定义
// 返回: []byte 字体数据, error 读取或集合错误 // 返回: []byte 字体数据, error 读取或集合错误
+40 -40
View File
@@ -23,6 +23,24 @@ import (
"strconv" "strconv"
) )
// type1StandardNames 保存Adobe标准编码的字形名称,用于复合字形引用
var type1StandardNames = func() [256]string {
var names [256]string
copy(names[32:127], cffStandardStrings[1:96])
for code, name := range map[byte]string{
161: "exclamdown", 162: "cent", 163: "sterling", 164: "fraction", 165: "yen", 166: "florin", 167: "section", 168: "currency",
169: "quotesingle", 170: "quotedblleft", 171: "guillemotleft", 172: "guilsinglleft", 173: "guilsinglright", 174: "fi", 175: "fl",
177: "endash", 178: "dagger", 179: "daggerdbl", 180: "periodcentered", 182: "paragraph", 183: "bullet", 184: "quotesinglbase",
185: "quotedblbase", 186: "quotedblright", 187: "guillemotright", 188: "ellipsis", 189: "perthousand", 191: "questiondown",
193: "grave", 194: "acute", 195: "circumflex", 196: "tilde", 197: "macron", 198: "breve", 199: "dotaccent", 200: "dieresis",
202: "ring", 203: "cedilla", 205: "hungarumlaut", 206: "ogonek", 207: "caron", 208: "emdash", 225: "AE", 227: "ordfeminine",
232: "Lslash", 233: "Oslash", 234: "OE", 235: "ordmasculine", 241: "ae", 245: "dotlessi", 248: "lslash", 249: "oslash", 250: "oe", 251: "germandbls",
} {
names[code] = name
}
return names
}()
// type1Program 保存解密后的Type1字形与子程序 // type1Program 保存解密后的Type1字形与子程序
type type1Program struct { type type1Program struct {
glyphs []type1Glyph glyphs []type1Glyph
@@ -42,6 +60,28 @@ type type1Scanner struct {
pos int pos int
} }
// type1Outline 保存字形指令的操作数栈及轮廓位置
type type1Outline struct {
program *type1Program
active map[string]bool
args []float64
postscript []float64
flexPoints [][2]float64
width float64
sideX float64
sideY float64
originX float64
originY float64
x float64
y float64
pointX float64
pointY float64
open bool
defined bool
flex bool
path bytes.Buffer
}
// parseType1Program 读取Type1字体的字形程序及子程序 // parseType1Program 读取Type1字体的字形程序及子程序
// 入参: data PFA或PFB字体数据 // 入参: data PFA或PFB字体数据
// 返回: type1Program 字形和子程序, error 错误信息 // 返回: type1Program 字形和子程序, error 错误信息
@@ -303,28 +343,6 @@ func type1BinaryStart(token string) bool {
return token == "RD" || token == "-|" return token == "RD" || token == "-|"
} }
// type1Outline 保存字形指令的操作数栈及轮廓位置
type type1Outline struct {
program *type1Program
active map[string]bool
args []float64
postscript []float64
flexPoints [][2]float64
width float64
sideX float64
sideY float64
originX float64
originY float64
x float64
y float64
pointX float64
pointY float64
open bool
defined bool
flex bool
path bytes.Buffer
}
// outline 将Type1字形指令转换为保留三次曲线的Type2程序 // outline 将Type1字形指令转换为保留三次曲线的Type2程序
// 入参: glyph 字形数据 // 入参: glyph 字形数据
// 返回: []byte Type2字形程序, error 错误信息 // 返回: []byte Type2字形程序, error 错误信息
@@ -638,24 +656,6 @@ func (s *type1Outline) component(code, x, y float64) error {
return fmt.Errorf("missing Type1 component %s", name) return fmt.Errorf("missing Type1 component %s", name)
} }
// type1StandardNames 保存Adobe标准编码的字形名称,用于复合字形引用
var type1StandardNames = func() [256]string {
var names [256]string
copy(names[32:127], cffStandardStrings[1:96])
for code, name := range map[byte]string{
161: "exclamdown", 162: "cent", 163: "sterling", 164: "fraction", 165: "yen", 166: "florin", 167: "section", 168: "currency",
169: "quotesingle", 170: "quotedblleft", 171: "guillemotleft", 172: "guilsinglleft", 173: "guilsinglright", 174: "fi", 175: "fl",
177: "endash", 178: "dagger", 179: "daggerdbl", 180: "periodcentered", 182: "paragraph", 183: "bullet", 184: "quotesinglbase",
185: "quotedblbase", 186: "quotedblright", 187: "guillemotright", 188: "ellipsis", 189: "perthousand", 191: "questiondown",
193: "grave", 194: "acute", 195: "circumflex", 196: "tilde", 197: "macron", 198: "breve", 199: "dotaccent", 200: "dieresis",
202: "ring", 203: "cedilla", 205: "hungarumlaut", 206: "ogonek", 207: "caron", 208: "emdash", 225: "AE", 227: "ordfeminine",
232: "Lslash", 233: "Oslash", 234: "OE", 235: "ordmasculine", 241: "ae", 245: "dotlessi", 248: "lslash", 249: "oslash", 250: "oe", 251: "germandbls",
} {
names[code] = name
}
return names
}()
// move 在Type2字形程序中建立新轮廓 // move 在Type2字形程序中建立新轮廓
// 返回: error 错误信息 // 返回: error 错误信息
func (s *type1Outline) move() error { func (s *type1Outline) move() error {
+10 -400
View File
@@ -21,6 +21,16 @@ import (
"sort" "sort"
) )
// otfTableRecord OTF表记录结构
// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据
type otfTableRecord struct {
tag string
checksum uint32
offset int
length int
data []byte
}
// extractCollectionFont 提取集合中的单个OpenType字体,保留表内容并重新计算校验和 // extractCollectionFont 提取集合中的单个OpenType字体,保留表内容并重新计算校验和
// 入参: data 集合数据, index 字体索引 // 入参: data 集合数据, index 字体索引
// 返回: []byte 单字体数据, error 错误信息 // 返回: []byte 单字体数据, error 错误信息
@@ -81,13 +91,6 @@ func fontFileTables(data []byte, index int) (map[string][]byte, error) {
return tables, nil return tables, nil
} }
// packedGlyphRune 获取包装字体字符
// 入参: gid 字形ID
// 返回: rune 包装字体字符
func packedGlyphRune(gid uint16) rune {
return 0xF0000 + rune(gid)
}
// calcTableChecksum 计算字体表校验和 // calcTableChecksum 计算字体表校验和
// 入参: data 表数据 // 入参: data 表数据
// 返回: uint32 校验和 // 返回: uint32 校验和
@@ -109,165 +112,6 @@ func calcTableChecksum(data []byte) uint32 {
return sum return sum
} }
// parseCmapMappings 解析cmap字符映射
// 入参: data cmap表数据
// 返回: map[rune]uint16 字符到字形映射
func parseCmapMappings(data []byte) map[rune]uint16 {
if len(data) < 4 {
return nil
}
numTables := int(binary.BigEndian.Uint16(data[2:4]))
result := make(map[rune]uint16)
for i := 0; i < numTables; i++ {
pos := 4 + i*8
if pos+8 > len(data) {
break
}
platformID := binary.BigEndian.Uint16(data[pos : pos+2])
if platformID != 0 && platformID != 3 {
continue
}
offset := int(binary.BigEndian.Uint32(data[pos+4 : pos+8]))
if offset+2 > len(data) {
continue
}
switch binary.BigEndian.Uint16(data[offset : offset+2]) {
case 0:
parseCmapFormat0(data[offset:], result)
case 4:
parseCmapFormat4(data[offset:], result)
case 6:
parseCmapFormat6(data[offset:], result)
case 12:
parseCmapFormat12(data[offset:], result)
}
}
if len(result) == 0 {
return nil
}
return result
}
// parseCmapFormat0 解析cmap format 0
// 入参: data 子表数据, result 字符映射
func parseCmapFormat0(data []byte, result map[rune]uint16) {
if len(data) < 262 {
return
}
for i := 0; i < 256; i++ {
gid := uint16(data[6+i])
if gid != 0 {
result[rune(i)] = gid
}
}
}
// parseCmapFormat4 解析cmap format 4
// 入参: data 子表数据, result 字符映射
func parseCmapFormat4(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := int(binary.BigEndian.Uint16(data[2:4]))
if length > len(data) {
length = len(data)
}
segCount := int(binary.BigEndian.Uint16(data[6:8]) / 2)
endPos := 14
startPos := endPos + segCount*2 + 2
deltaPos := startPos + segCount*2
rangePos := deltaPos + segCount*2
if rangePos+segCount*2 > length {
return
}
for i := 0; i < segCount; i++ {
end := binary.BigEndian.Uint16(data[endPos+i*2:])
start := binary.BigEndian.Uint16(data[startPos+i*2:])
delta := int16(binary.BigEndian.Uint16(data[deltaPos+i*2:]))
rangeOffsetPos := rangePos + i*2
rangeOffset := binary.BigEndian.Uint16(data[rangeOffsetPos:])
if start == 0xFFFF && end == 0xFFFF {
continue
}
for c := start; c <= end; c++ {
var gid uint16
if rangeOffset == 0 {
gid = uint16(int(c) + int(delta))
} else {
gidPos := rangeOffsetPos + int(rangeOffset) + int(c-start)*2
if gidPos+2 > length {
continue
}
gid = binary.BigEndian.Uint16(data[gidPos:])
if gid != 0 {
gid = uint16(int(gid) + int(delta))
}
}
if gid != 0 {
result[rune(c)] = gid
}
if c == 0xFFFF {
break
}
}
}
}
// parseCmapFormat6 解析cmap format 6
// 入参: data 子表数据, result 字符映射
func parseCmapFormat6(data []byte, result map[rune]uint16) {
if len(data) < 10 {
return
}
firstCode := int(binary.BigEndian.Uint16(data[6:8]))
entryCount := int(binary.BigEndian.Uint16(data[8:10]))
for i := 0; i < entryCount && 10+i*2+2 <= len(data); i++ {
gid := binary.BigEndian.Uint16(data[10+i*2:])
if gid != 0 {
result[rune(firstCode+i)] = gid
}
}
}
// parseCmapFormat12 解析cmap format 12
// 入参: data 子表数据, result 字符映射
func parseCmapFormat12(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := int(binary.BigEndian.Uint32(data[4:8]))
if length > len(data) {
length = len(data)
}
nGroups := int(binary.BigEndian.Uint32(data[12:16]))
for i := 0; i < nGroups; i++ {
pos := 16 + i*12
if pos+12 > length {
break
}
startChar := binary.BigEndian.Uint32(data[pos:])
endChar := binary.BigEndian.Uint32(data[pos+4:])
startGID := binary.BigEndian.Uint32(data[pos+8:])
for c := startChar; c <= endChar; c++ {
gid := startGID + c - startChar
if gid != 0 && gid <= 0xFFFF {
result[rune(c)] = uint16(gid)
}
if c == endChar {
break
}
}
}
}
// addPackedGlyphMapping 添加字形ID私有映射
// 入参: mapping 字符映射, numGlyphs 字形数量
func addPackedGlyphMapping(mapping map[rune]uint16, numGlyphs uint16) {
for i := uint16(0); i < numGlyphs; i++ {
mapping[packedGlyphRune(i)] = i
}
}
// buildHeadTable 构建head表 // buildHeadTable 构建head表
// 入参: unitsPerEm 每em单位数 // 入参: unitsPerEm 每em单位数
// 返回: []byte head表数据 // 返回: []byte head表数据
@@ -436,240 +280,6 @@ func buildHmtxTable(widths []uint16) []byte {
return buf.Bytes() return buf.Bytes()
} }
// cmapSegment cmap表段结构
// 字段: start 开始字符, end 结束字符, delta 增量, offset 偏移
type cmapSegment struct {
start, end uint16
delta int16
offset uint16
}
// buildCmapTable 构建cmap表 (Format 4)
// 入参: numGlyphs 字形数量, mapping 字符映射
// 返回: []byte cmap表数据
func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
if shouldBuildCmapFormat12(mapping) {
return buildCmapTableFormat12(numGlyphs, mapping)
}
var segs []cmapSegment
if mapping == nil {
end := uint16(0xFFFF)
if numGlyphs > 0 {
end = numGlyphs - 1
}
segs = append(segs, cmapSegment{start: 0, end: end, delta: 0, offset: 0})
} else {
var codes []int
for r := range mapping {
if r <= 0xFFFF {
codes = append(codes, int(r))
}
}
sort.Ints(codes)
if len(codes) > 0 {
start := codes[0]
prev := start
for i := 1; i < len(codes); i++ {
curr := codes[i]
if curr != prev+1 {
segs = append(segs, cmapSegment{start: uint16(start), end: uint16(prev), delta: 0, offset: 0})
start = curr
}
prev = curr
}
segs = append(segs, cmapSegment{start: uint16(start), end: uint16(prev), delta: 0, offset: 0})
}
}
segs = append(segs, cmapSegment{start: 0xFFFF, end: 0xFFFF, delta: 1, offset: 0})
segCount := uint16(len(segs))
searchRange := uint16(1)
entrySelector := uint16(0)
for searchRange*2 <= segCount {
searchRange *= 2
entrySelector++
}
searchRange *= 2
rangeShift := segCount*2 - searchRange
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint16(4))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(segCount*2))
binary.Write(buf, binary.BigEndian, searchRange)
binary.Write(buf, binary.BigEndian, entrySelector)
binary.Write(buf, binary.BigEndian, rangeShift)
var endCounts, startCounts, idDeltas, idRangeOffsets []uint16
var glyphIds []uint16
for _, s := range segs {
endCounts = append(endCounts, s.end)
startCounts = append(startCounts, s.start)
if mapping == nil {
idDeltas = append(idDeltas, 0)
idRangeOffsets = append(idRangeOffsets, 0)
continue
}
if s.start == 0xFFFF {
idDeltas = append(idDeltas, 1)
idRangeOffsets = append(idRangeOffsets, 0)
continue
}
idDeltas = append(idDeltas, 0)
currentGlyphIdx := len(glyphIds)
for c := int(s.start); c <= int(s.end); c++ {
gid := mapping[rune(c)]
glyphIds = append(glyphIds, gid)
}
offset := (int(segCount)-int(len(idRangeOffsets))-1)*2 + 2 + currentGlyphIdx*2
idRangeOffsets = append(idRangeOffsets, uint16(offset))
}
for _, v := range endCounts {
binary.Write(buf, binary.BigEndian, v)
}
binary.Write(buf, binary.BigEndian, uint16(0))
for _, v := range startCounts {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range idDeltas {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range idRangeOffsets {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range glyphIds {
binary.Write(buf, binary.BigEndian, v)
}
data := buf.Bytes()
binary.BigEndian.PutUint16(data[2:4], uint16(len(data)))
mainBuf := new(bytes.Buffer)
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(1))
binary.Write(mainBuf, binary.BigEndian, uint16(3))
binary.Write(mainBuf, binary.BigEndian, uint16(1))
binary.Write(mainBuf, binary.BigEndian, uint32(12))
mainBuf.Write(data)
return mainBuf.Bytes()
}
// shouldBuildCmapFormat12 判断终止码、补充平面字符或format 4长度溢出时是否需要format 12
// 入参: mapping 字符到字形的映射
// 返回: bool 是否需要构建format 12子表
func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
if mapping == nil {
return false
}
var codes []int
for r := range mapping {
if r >= 0xFFFF {
return true
}
codes = append(codes, int(r))
}
if len(codes) == 0 {
return false
}
sort.Ints(codes)
segCount := 1
glyphIDCount := 1
prev := codes[0]
for i := 1; i < len(codes); i++ {
curr := codes[i]
if curr == prev {
continue
}
if curr != prev+1 {
segCount++
}
glyphIDCount++
prev = curr
}
segCount++
length := 16 + segCount*8 + glyphIDCount*2
return length > 0xFFFF
}
// buildCmapTableFormat12 构建cmap表 (Format 12)
// 入参: numGlyphs 字形数量, mapping 字符映射
// 返回: []byte cmap表数据
func buildCmapTableFormat12(numGlyphs uint16, mapping map[rune]uint16) []byte {
var codes []int
if mapping == nil {
for i := 0; i < int(numGlyphs); i++ {
codes = append(codes, i)
}
} else {
for r := range mapping {
if r >= 0 {
codes = append(codes, int(r))
}
}
}
sort.Ints(codes)
type cmapGroup struct {
startChar uint32
endChar uint32
startGID uint32
}
var groups []cmapGroup
for i := 0; i < len(codes); {
r := rune(codes[i])
gid := uint16(codes[i])
if mapping != nil {
gid = mapping[r]
}
group := cmapGroup{startChar: uint32(r), endChar: uint32(r), startGID: uint32(gid)}
prevRune := r
prevGID := gid
i++
for i < len(codes) {
nextRune := rune(codes[i])
nextGID := uint16(codes[i])
if mapping != nil {
nextGID = mapping[nextRune]
}
if nextRune != prevRune+1 || nextGID != prevGID+1 {
break
}
group.endChar = uint32(nextRune)
prevRune = nextRune
prevGID = nextGID
i++
}
groups = append(groups, group)
}
sub := new(bytes.Buffer)
binary.Write(sub, binary.BigEndian, uint16(12))
binary.Write(sub, binary.BigEndian, uint16(0))
binary.Write(sub, binary.BigEndian, uint32(16+12*len(groups)))
binary.Write(sub, binary.BigEndian, uint32(0))
binary.Write(sub, binary.BigEndian, uint32(len(groups)))
for _, group := range groups {
binary.Write(sub, binary.BigEndian, group.startChar)
binary.Write(sub, binary.BigEndian, group.endChar)
binary.Write(sub, binary.BigEndian, group.startGID)
}
mainBuf := new(bytes.Buffer)
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(2))
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(4))
binary.Write(mainBuf, binary.BigEndian, uint32(20))
binary.Write(mainBuf, binary.BigEndian, uint16(3))
binary.Write(mainBuf, binary.BigEndian, uint16(10))
binary.Write(mainBuf, binary.BigEndian, uint32(20))
mainBuf.Write(sub.Bytes())
return mainBuf.Bytes()
}
// otfTableRecord OTF表记录结构
// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据
type otfTableRecord struct {
tag string
checksum uint32
offset int
length int
data []byte
}
// serializeOTF 序列化OpenType字体结构 // serializeOTF 序列化OpenType字体结构
// 入参: tables 表数据映射 // 入参: tables 表数据映射
// 返回: []byte 完整字体数据, error 错误信息 // 返回: []byte 完整字体数据, error 错误信息
+27 -27
View File
@@ -22,6 +22,9 @@ import (
"strings" "strings"
) )
// IdentityMatrix 单位矩阵
var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0}
// Box 矩形区域 // Box 矩形区域
type Box struct { type Box struct {
X, Y, W, H float64 X, Y, W, H float64
@@ -32,11 +35,9 @@ type Point struct {
X, Y float64 X, Y float64
} }
// geometryEqual 比较毫米坐标,使用固定绝对误差,不受绘图库全局精度影响 // Matrix 2D仿射变换矩阵
// 入参: a、b 待比较的坐标 type Matrix struct {
// 返回: bool 是否在容差内相等 a, b, c, d, e, f float64
func geometryEqual(a, b float64) bool {
return math.Abs(a-b) <= 1e-10
} }
// ParseBox 解析Box字符串 // ParseBox 解析Box字符串
@@ -62,14 +63,6 @@ func ParseBox(s string) (Box, error) {
return Box{X: values[0], Y: values[1], W: values[2], H: values[3]}, nil return Box{X: values[0], Y: values[1], W: values[2], H: values[3]}, nil
} }
// Matrix 2D仿射变换矩阵
type Matrix struct {
a, b, c, d, e, f float64
}
// IdentityMatrix 单位矩阵
var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0}
// MatrixFromValues 从a、b、c、d、e、f构造矩阵,不经过文本格式化 // MatrixFromValues 从a、b、c、d、e、f构造矩阵,不经过文本格式化
// 入参: values 矩阵分量 // 入参: values 矩阵分量
// 返回: Matrix 变换矩阵 // 返回: Matrix 变换矩阵
@@ -140,20 +133,6 @@ func (m Matrix) String() string {
return fmt.Sprintf("%s %s %s %s %s %s", ofdNumber(m.a), ofdNumber(m.b), ofdNumber(m.c), ofdNumber(m.d), ofdNumber(m.e), ofdNumber(m.f)) return fmt.Sprintf("%s %s %s %s %s %s", ofdNumber(m.a), ofdNumber(m.b), ofdNumber(m.c), ofdNumber(m.d), ofdNumber(m.e), ofdNumber(m.f))
} }
// axisAlignedMatrix 判断变换是否保持坐标轴平行,含直角旋转与镜像
// 入参: m 仿射变换矩阵
// 返回: bool 是否为非退化的轴对齐变换
func axisAlignedMatrix(m Matrix) bool {
return m.b == 0 && m.c == 0 && m.a != 0 && m.d != 0 || m.a == 0 && m.d == 0 && m.b != 0 && m.c != 0
}
// matrixVector 变换位移,不包含平移分量
// 入参: m 仿射变换矩阵, x、y 原始位移
// 返回: float64 变换后的X位移, float64 变换后的Y位移
func matrixVector(m Matrix, x, y float64) (float64, float64) {
return m.a*x + m.c*y, m.b*x + m.d*y
}
// Invert 求逆矩阵 // Invert 求逆矩阵
// 返回: Matrix 逆矩阵, bool 是否可逆 // 返回: Matrix 逆矩阵, bool 是否可逆
func (m Matrix) Invert() (Matrix, bool) { func (m Matrix) Invert() (Matrix, bool) {
@@ -184,6 +163,27 @@ func (m Matrix) YScale() float64 {
return math.Sqrt(m.c*m.c + m.d*m.d) return math.Sqrt(m.c*m.c + m.d*m.d)
} }
// geometryEqual 比较毫米坐标,使用固定绝对误差,不受绘图库全局精度影响
// 入参: a、b 待比较的坐标
// 返回: bool 是否在容差内相等
func geometryEqual(a, b float64) bool {
return math.Abs(a-b) <= 1e-10
}
// axisAlignedMatrix 判断变换是否保持坐标轴平行,含直角旋转与镜像
// 入参: m 仿射变换矩阵
// 返回: bool 是否为非退化的轴对齐变换
func axisAlignedMatrix(m Matrix) bool {
return m.b == 0 && m.c == 0 && m.a != 0 && m.d != 0 || m.a == 0 && m.d == 0 && m.b != 0 && m.c != 0
}
// matrixVector 变换位移,不包含平移分量
// 入参: m 仿射变换矩阵, x、y 原始位移
// 返回: float64 变换后的X位移, float64 变换后的Y位移
func matrixVector(m Matrix, x, y float64) (float64, float64) {
return m.a*x + m.c*y, m.b*x + m.d*y
}
// parseFloats 解析浮点数数组 // parseFloats 解析浮点数数组
// 入参: s 字符串 // 入参: s 字符串
// 返回: []float64 浮点数数组 // 返回: []float64 浮点数数组
+24 -24
View File
@@ -19,9 +19,6 @@ import (
"math" "math"
) )
// GeometryOperation 指定路径填充区域的布尔运算
type GeometryOperation uint8
const ( const (
GeometryIntersect GeometryOperation = iota GeometryIntersect GeometryOperation = iota
GeometryUnion GeometryUnion
@@ -29,6 +26,9 @@ const (
GeometryXor GeometryXor
) )
// GeometryOperation 指定路径填充区域的布尔运算
type GeometryOperation uint8
// StrokeOptions 描述描边参数,长度单位为毫米 // StrokeOptions 描述描边参数,长度单位为毫米
type StrokeOptions struct { type StrokeOptions struct {
// Width为正有限描边宽度 // Width为正有限描边宽度
@@ -64,27 +64,6 @@ type GeometryCurves interface {
Curves(path GeometryPath) (GeometryPath, error) Curves(path GeometryPath) (GeometryPath, error)
} }
// validateStroke 校验各后端共用的描边参数
// 入参: options 描边样式
// 返回: error 无效样式
func validateStroke(options StrokeOptions) error {
if !finite(options.Width) || options.Width <= 0 || !finite(options.Tolerance) || options.Tolerance < 0 || !finite(options.DashOffset) || !finite(options.MiterLimit) || options.MiterLimit < 0 {
return fmt.Errorf("invalid stroke width or tolerance")
}
if options.Cap != "" && options.Cap != "Butt" && options.Cap != "Round" && options.Cap != "Square" {
return fmt.Errorf("invalid stroke cap %q", options.Cap)
}
if options.Join != "" && options.Join != "Miter" && options.Join != "Round" && options.Join != "Bevel" {
return fmt.Errorf("invalid stroke join %q", options.Join)
}
for _, dash := range options.Dashes {
if !finite(dash) || dash < 0 {
return fmt.Errorf("invalid stroke dash")
}
}
return nil
}
// Geometry 返回当前几何后端,不隐式恢复默认实现 // Geometry 返回当前几何后端,不隐式恢复默认实现
// 返回: GeometryBackend 几何后端, error 未配置错误 // 返回: GeometryBackend 几何后端, error 未配置错误
func (r *Renderer) Geometry() (GeometryBackend, error) { func (r *Renderer) Geometry() (GeometryBackend, error) {
@@ -118,6 +97,27 @@ func (r *Renderer) PathOutline(object PathObject) (string, error) {
return path.SVG() return path.SVG()
} }
// validateStroke 校验各后端共用的描边参数
// 入参: options 描边样式
// 返回: error 无效样式
func validateStroke(options StrokeOptions) error {
if !finite(options.Width) || options.Width <= 0 || !finite(options.Tolerance) || options.Tolerance < 0 || !finite(options.DashOffset) || !finite(options.MiterLimit) || options.MiterLimit < 0 {
return fmt.Errorf("invalid stroke width or tolerance")
}
if options.Cap != "" && options.Cap != "Butt" && options.Cap != "Round" && options.Cap != "Square" {
return fmt.Errorf("invalid stroke cap %q", options.Cap)
}
if options.Join != "" && options.Join != "Miter" && options.Join != "Round" && options.Join != "Bevel" {
return fmt.Errorf("invalid stroke join %q", options.Join)
}
for _, dash := range options.Dashes {
if !finite(dash) || dash < 0 {
return fmt.Errorf("invalid stroke dash")
}
}
return nil
}
// geometryRectangle 构建页面坐标矩形,保留输入精度 // geometryRectangle 构建页面坐标矩形,保留输入精度
// 入参: box 矩形范围 // 入参: box 矩形范围
// 返回: GeometryPath 闭合路径 // 返回: GeometryPath 闭合路径
+19 -19
View File
@@ -20,13 +20,6 @@ import (
"strings" "strings"
) )
// GeometryPath 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
type GeometryPath []GeometrySegment
// GeometryVerb 表示几何路径指令
type GeometryVerb uint8
const ( const (
GeometryMove GeometryVerb = iota GeometryMove GeometryVerb = iota
GeometryLine GeometryLine
@@ -36,6 +29,13 @@ const (
GeometryClose GeometryClose
) )
// GeometryPath 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
type GeometryPath []GeometrySegment
// GeometryVerb 表示几何路径指令
type GeometryVerb uint8
// GeometrySegment 保存路径端点、贝塞尔控制点或端点式椭圆弧参数 // GeometrySegment 保存路径端点、贝塞尔控制点或端点式椭圆弧参数
// 弧线Rotation单位为度,Sweep为true表示顺时针,Large表示大弧 // 弧线Rotation单位为度,Sweep为true表示顺时针,Large表示大弧
type GeometrySegment struct { type GeometrySegment struct {
@@ -117,6 +117,18 @@ func ParseGeometryPath(data string) (GeometryPath, error) {
return result, nil return result, nil
} }
// SVG 将几何路径编码为SVG路径,不改变曲线或坐标精度
// 返回: string SVG路径, error 不支持的路径指令
func (p GeometryPath) SVG() (string, error) {
return p.encode(true)
}
// OFD 将几何路径编码为OFD紧缩路径
// 返回: string 紧缩路径, error 不支持的路径指令
func (p GeometryPath) OFD() (string, error) {
return p.encode(false)
}
// objectGeometryPath 解析对象路径并由当前几何实现应用页面变换 // objectGeometryPath 解析对象路径并由当前几何实现应用页面变换
// 入参: geometry 几何后端, object 路径对象 // 入参: geometry 几何后端, object 路径对象
// 返回: GeometryPath 页面路径, error 参数或变换错误 // 返回: GeometryPath 页面路径, error 参数或变换错误
@@ -156,18 +168,6 @@ func closedGeometry(path GeometryPath) GeometryPath {
return result return result
} }
// SVG 将几何路径编码为SVG路径,不改变曲线或坐标精度
// 返回: string SVG路径, error 不支持的路径指令
func (p GeometryPath) SVG() (string, error) {
return p.encode(true)
}
// OFD 将几何路径编码为OFD紧缩路径
// 返回: string 紧缩路径, error 不支持的路径指令
func (p GeometryPath) OFD() (string, error) {
return p.encode(false)
}
// encode 按目标格式写入路径,圆弧标志采用两种格式均支持的0和1 // encode 按目标格式写入路径,圆弧标志采用两种格式均支持的0和1
// 入参: svg 是否使用SVG指令 // 入参: svg 是否使用SVG指令
// 返回: string 路径数据, error 不支持的路径指令 // 返回: string 路径数据, error 不支持的路径指令
+35 -35
View File
@@ -20,41 +20,6 @@ import (
"slices" "slices"
) )
// validate 校验公共路径指令、子路径起点和有限坐标
// 返回: error 无效路径
func (p GeometryPath) validate() error {
started := false
for _, s := range p {
points := []Point{s.End}
switch s.Verb {
case GeometryMove:
started = true
case GeometryLine:
case GeometryQuad:
points = append(points, s.Control1)
case GeometryCubic:
points = append(points, s.Control1, s.Control2)
case GeometryArc:
if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 {
return fmt.Errorf("invalid arc parameters")
}
case GeometryClose:
points = nil
default:
return fmt.Errorf("unsupported geometry command %d", s.Verb)
}
if !started {
return fmt.Errorf("geometry path must start with move")
}
for _, point := range points {
if !finite(point.X) || !finite(point.Y) {
return fmt.Errorf("non-finite geometry coordinate")
}
}
}
return nil
}
// Transform 精确变换控制点和椭圆轴,退化弧沿投影极值分段保留折返 // Transform 精确变换控制点和椭圆轴,退化弧沿投影极值分段保留折返
// 入参: matrix 仿射变换 // 入参: matrix 仿射变换
// 返回: GeometryPath 独立路径, error 无效路径或数值溢出 // 返回: GeometryPath 独立路径, error 无效路径或数值溢出
@@ -158,3 +123,38 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
} }
return result, nil return result, nil
} }
// validate 校验公共路径指令、子路径起点和有限坐标
// 返回: error 无效路径
func (p GeometryPath) validate() error {
started := false
for _, s := range p {
points := []Point{s.End}
switch s.Verb {
case GeometryMove:
started = true
case GeometryLine:
case GeometryQuad:
points = append(points, s.Control1)
case GeometryCubic:
points = append(points, s.Control1, s.Control2)
case GeometryArc:
if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 {
return fmt.Errorf("invalid arc parameters")
}
case GeometryClose:
points = nil
default:
return fmt.Errorf("unsupported geometry command %d", s.Verb)
}
if !started {
return fmt.Errorf("geometry path must start with move")
}
for _, point := range points {
if !finite(point.X) || !finite(point.Y) {
return fmt.Errorf("non-finite geometry coordinate")
}
}
}
return nil
}
+101 -101
View File
@@ -29,6 +29,107 @@ import (
_ "golang.org/x/image/tiff" _ "golang.org/x/image/tiff"
) )
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
type transparentEdgeImage struct {
source *image.NRGBA
rect image.Rectangle
}
// transparentEdgeImage64 按需补齐十六位像素边缘,保留原始只读图像
type transparentEdgeImage64 struct {
source image.Image
rect image.Rectangle
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage) ColorModel() color.Model { return color.NRGBAModel }
// NRGBAAt 读取原始像素或按邻域补齐透明边缘,边界外透明
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA 非预乘颜色
func (s *transparentEdgeImage) NRGBAAt(x, y int) color.NRGBA {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := s.source.NRGBAAt(sx, sy)
if c.A == 0 {
if edge, ok := transparentEdgeColor(s.source, sx, sy); ok {
c = edge
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c = transparentPaddingColor(c)
}
return c
}
// At 返回非预乘像素颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *transparentEdgeImage) At(x, y int) color.Color { return s.NRGBAAt(x, y) }
// RGBA64At 返回预乘十六位像素,供采样器无装箱读取
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBAAt(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage64) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回十六位非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage64) ColorModel() color.Model { return color.NRGBA64Model }
// At 读取十六位原始颜色,透明邻域使用最小非零透明度保留边缘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 非预乘颜色
func (s *transparentEdgeImage64) At(x, y int) color.Color {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA64{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := color.NRGBA64Model.Convert(s.source.At(sx, sy)).(color.NRGBA64)
if c.A == 0 {
var best color.NRGBA64
for ny := max(sy-1, b.Min.Y); ny < min(sy+2, b.Max.Y); ny++ {
for nx := max(sx-1, b.Min.X); nx < min(sx+2, b.Max.X); nx++ {
v := color.NRGBA64Model.Convert(s.source.At(nx, ny)).(color.NRGBA64)
if v.A > best.A {
best = v
}
}
}
if best.A != 0 {
best.A = 1
c = best
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c.A = 1
}
return c
}
// RGBA64At 返回保留原采样精度的预乘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage64) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// decodeImageData 解码图片数据 // decodeImageData 解码图片数据
// 入参: data 图片数据 // 入参: data 图片数据
// 返回: image.Image 图片对象, string 图片格式, error 错误信息 // 返回: image.Image 图片对象, string 图片格式, error 错误信息
@@ -466,54 +567,6 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) {
return &transparentEdgeImage{source: src, rect: image.Rect(0, 0, w+2, h+2)}, 1 return &transparentEdgeImage{source: src, rect: image.Rect(0, 0, w+2, h+2)}, 1
} }
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
type transparentEdgeImage struct {
source *image.NRGBA
rect image.Rectangle
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage) ColorModel() color.Model { return color.NRGBAModel }
// NRGBAAt 读取原始像素或按邻域补齐透明边缘,边界外透明
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA 非预乘颜色
func (s *transparentEdgeImage) NRGBAAt(x, y int) color.NRGBA {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := s.source.NRGBAAt(sx, sy)
if c.A == 0 {
if edge, ok := transparentEdgeColor(s.source, sx, sy); ok {
c = edge
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c = transparentPaddingColor(c)
}
return c
}
// At 返回非预乘像素颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *transparentEdgeImage) At(x, y int) color.Color { return s.NRGBAAt(x, y) }
// RGBA64At 返回预乘十六位像素,供采样器无装箱读取
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBAAt(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// imageWithTransparentEdge64 保留十六位颜色和透明度,不经八位图像中转 // imageWithTransparentEdge64 保留十六位颜色和透明度,不经八位图像中转
// 入参: original 原始图像, source 已解码的像素源 // 入参: original 原始图像, source 已解码的像素源
// 返回: image.Image 边缘处理结果, int 补齐像素数 // 返回: image.Image 边缘处理结果, int 补齐像素数
@@ -538,56 +591,3 @@ func imageWithTransparentEdge64(original, source image.Image) (image.Image, int)
} }
return &transparentEdgeImage64{source: source, rect: image.Rect(0, 0, bounds.Dx()+2, bounds.Dy()+2)}, 1 return &transparentEdgeImage64{source: source, rect: image.Rect(0, 0, bounds.Dx()+2, bounds.Dy()+2)}, 1
} }
// transparentEdgeImage64 按需补齐十六位像素边缘,保留原始只读图像
type transparentEdgeImage64 struct {
source image.Image
rect image.Rectangle
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage64) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回十六位非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage64) ColorModel() color.Model { return color.NRGBA64Model }
// At 读取十六位原始颜色,透明邻域使用最小非零透明度保留边缘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 非预乘颜色
func (s *transparentEdgeImage64) At(x, y int) color.Color {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA64{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := color.NRGBA64Model.Convert(s.source.At(sx, sy)).(color.NRGBA64)
if c.A == 0 {
var best color.NRGBA64
for ny := max(sy-1, b.Min.Y); ny < min(sy+2, b.Max.Y); ny++ {
for nx := max(sx-1, b.Min.X); nx < min(sx+2, b.Max.X); nx++ {
v := color.NRGBA64Model.Convert(s.source.At(nx, ny)).(color.NRGBA64)
if v.A > best.A {
best = v
}
}
}
if best.A != 0 {
best.A = 1
c = best
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c.A = 1
}
return c
}
// RGBA64At 返回保留原采样精度的预乘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage64) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
+14 -14
View File
@@ -43,20 +43,6 @@ func NewEncodedImage(data []byte) (*EncodedImage, error) {
return newEncodedImage(bytes.Clone(data)) return newEncodedImage(bytes.Clone(data))
} }
// newEncodedImage 接管编码数据并读取头信息
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func newEncodedImage(data []byte) (*EncodedImage, error) {
config, format, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return nil, err
}
if format != "jpeg" && format != "png" {
return nil, fmt.Errorf("unsupported encoded image %q", format)
}
return &EncodedImage{data: data, format: format, config: config}, nil
}
// Bytes 返回只读原始编码,调用方不得修改 // Bytes 返回只读原始编码,调用方不得修改
// 返回: []byte 原始编码 // 返回: []byte 原始编码
func (img *EncodedImage) Bytes() []byte { return img.data } func (img *EncodedImage) Bytes() []byte { return img.data }
@@ -105,6 +91,20 @@ func (r *Renderer) ImageResource(id string) (image.Image, error) {
return r.cachedImageResource(path) return r.cachedImageResource(path)
} }
// newEncodedImage 接管编码数据并读取头信息
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func newEncodedImage(data []byte) (*EncodedImage, error) {
config, format, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return nil, err
}
if format != "jpeg" && format != "png" {
return nil, fmt.Errorf("unsupported encoded image %q", format)
}
return &EncodedImage{data: data, format: format, config: config}, nil
}
// cachedImageResource 复用原始图片资源,不依赖输出后端 // cachedImageResource 复用原始图片资源,不依赖输出后端
// 入参: resPath 图片资源路径 // 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 读取错误 // 返回: image.Image 图片对象, error 读取错误
+68 -68
View File
@@ -19,6 +19,15 @@ import (
"strconv" "strconv"
) )
// graphicObjectTarget 图形对象集合
type graphicObjectTarget struct {
objects *[]GraphicObject
text *[]TextObject
path *[]PathObject
image *[]ImageObject
composite *[]CompositeGraphicUnit
}
// Actions 返回对象动作的独立副本,不修改原对象 // Actions 返回对象动作的独立副本,不修改原对象
// 返回: []Action 动作列表 // 返回: []Action 动作列表
func (o GraphicObject) Actions() []Action { func (o GraphicObject) Actions() []Action {
@@ -113,13 +122,65 @@ func (c *StrokeColor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
return (*FillColor)(c).UnmarshalXML(d, start) return (*FillColor)(c).UnmarshalXML(d, start)
} }
// graphicObjectTarget 图形对象集合 // UnmarshalXML 解析图层并保留对象顺序
type graphicObjectTarget struct { // 入参: d XML解码器, start 起始节点
objects *[]GraphicObject // 返回: error 错误信息
text *[]TextObject func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
path *[]PathObject *l = Layer{}
image *[]ImageObject l.ID = attrValue(start, "ID")
composite *[]CompositeGraphicUnit l.Type = attrValue(start, "Type")
l.DrawParam = attrValue(start, "DrawParam")
return decodeObjectContainer(d, start, l.decodeObject)
}
// UnmarshalXML 解析复合图元并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*c = CompositeGraphicUnit{}
c.ID = attrValue(start, "ID")
c.BaseLoc = attrValue(start, "BaseLoc")
c.ResourceID = attrValue(start, "ResourceID")
c.Boundary = attrValue(start, "Boundary")
c.CTM = attrValue(start, "CTM")
c.DrawParam = attrValue(start, "DrawParam")
if value := attrValue(start, "Alpha"); value != "" {
if alpha, err := strconv.Atoi(value); err == nil {
c.Alpha = &alpha
}
}
if value := attrValue(start, "Visible"); value != "" {
if visible, err := strconv.ParseBool(value); err == nil {
c.Visible = &visible
}
}
return decodeObjectContainer(d, start, c.decodeObject)
}
// UnmarshalXML 解析注释外观并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*a = Appearance{}
a.Boundary = attrValue(start, "Boundary")
return decodeObjectContainer(d, start, a.decodeObject)
}
// UnmarshalXML 解析图案单元内容并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*p = PatternContent{}
return decodeObjectContainer(d, start, p.decodeObject)
}
// UnmarshalXML 解析渐变分段并保留位置的缺省状态
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (s *ShdSegment) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type segment ShdSegment
*s = ShdSegment{positionMissing: attrValue(start, "Position") == ""}
return d.DecodeElement((*segment)(s), &start)
} }
// decodeGraphicObject 解析图形对象 // decodeGraphicObject 解析图形对象
@@ -183,17 +244,6 @@ func decodeObjectContainer(d *xml.Decoder, start xml.StartElement, decode func(*
} }
} }
// UnmarshalXML 解析图层并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*l = Layer{}
l.ID = attrValue(start, "ID")
l.Type = attrValue(start, "Type")
l.DrawParam = attrValue(start, "DrawParam")
return decodeObjectContainer(d, start, l.decodeObject)
}
// decodeObject 解析图层子对象 // decodeObject 解析图层子对象
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
@@ -214,30 +264,6 @@ func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error {
return d.Skip() return d.Skip()
} }
// UnmarshalXML 解析复合图元并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*c = CompositeGraphicUnit{}
c.ID = attrValue(start, "ID")
c.BaseLoc = attrValue(start, "BaseLoc")
c.ResourceID = attrValue(start, "ResourceID")
c.Boundary = attrValue(start, "Boundary")
c.CTM = attrValue(start, "CTM")
c.DrawParam = attrValue(start, "DrawParam")
if value := attrValue(start, "Alpha"); value != "" {
if alpha, err := strconv.Atoi(value); err == nil {
c.Alpha = &alpha
}
}
if value := attrValue(start, "Visible"); value != "" {
if visible, err := strconv.ParseBool(value); err == nil {
c.Visible = &visible
}
}
return decodeObjectContainer(d, start, c.decodeObject)
}
// decodeObject 解析复合图元子对象 // decodeObject 解析复合图元子对象
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
@@ -275,15 +301,6 @@ func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartEleme
return nil return nil
} }
// UnmarshalXML 解析注释外观并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*a = Appearance{}
a.Boundary = attrValue(start, "Boundary")
return decodeObjectContainer(d, start, a.decodeObject)
}
// decodeObject 解析注释外观子对象 // decodeObject 解析注释外观子对象
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
@@ -304,14 +321,6 @@ func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error
return d.Skip() return d.Skip()
} }
// UnmarshalXML 解析图案单元内容并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*p = PatternContent{}
return decodeObjectContainer(d, start, p.decodeObject)
}
// decodeObject 解析图案单元内容子对象 // decodeObject 解析图案单元内容子对象
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
@@ -332,15 +341,6 @@ func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) er
return d.Skip() return d.Skip()
} }
// UnmarshalXML 解析渐变分段并保留位置的缺省状态
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (s *ShdSegment) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type segment ShdSegment
*s = ShdSegment{positionMissing: attrValue(start, "Position") == ""}
return d.DecodeElement((*segment)(s), &start)
}
// attrValue 获取XML属性值 // attrValue 获取XML属性值
// 入参: start 起始节点, name 属性名 // 入参: start 起始节点, name 属性名
// 返回: string 属性值 // 返回: string 属性值
+20 -20
View File
@@ -21,6 +21,13 @@ import (
"strings" "strings"
) )
// PatternPaint 底纹画刷
type PatternPaint struct {
*Pattern
Color FillColor
Alpha *int
}
// ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA // ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA
// 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度 // 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度
// 返回: color.RGBA 标准颜色 // 返回: color.RGBA 标准颜色
@@ -28,6 +35,19 @@ func (r *Renderer) ResolveColor(value string, index *int, space string, alpha *i
return colorToRGBA(r.parseColorWithAlpha(value, index, space, alpha)) return colorToRGBA(r.parseColorWithAlpha(value, index, space, alpha))
} }
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
// 入参: segments 渐变分段, alpha 透明度
// 返回: []ColorStop 后端无关的渐变分段
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop {
var gradient []ColorStop
positions := gradientPositions(segments)
for i, segment := range segments {
segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha)
gradient = append(gradient, ColorStop{Offset: positions[i], Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)})
}
return gradient
}
// parseColorWithAlpha 解析带透明度的颜色 // parseColorWithAlpha 解析带透明度的颜色
// 入参: value 颜色值, index 调色板索引, space 颜色空间标识, alpha 透明度 // 入参: value 颜色值, index 调色板索引, space 颜色空间标识, alpha 透明度
// 返回: color.Color 颜色对象 // 返回: color.Color 颜色对象
@@ -204,13 +224,6 @@ func (r *Renderer) parseStrokeColor(strokeColor *StrokeColor) color.Color {
return r.parseFillColor((*FillColor)(strokeColor)) return r.parseFillColor((*FillColor)(strokeColor))
} }
// PatternPaint 底纹画刷
type PatternPaint struct {
*Pattern
Color FillColor
Alpha *int
}
// parsePatternPaint 解析底纹画刷 // parsePatternPaint 解析底纹画刷
// 入参: fill 填充颜色节点 // 入参: fill 填充颜色节点
// 返回: *PatternPaint 底纹画刷 // 返回: *PatternPaint 底纹画刷
@@ -232,19 +245,6 @@ func (r *Renderer) parseShdColor(segments []ShdSegment, alpha *int) color.Color
return r.parseColorWithAlpha(c.Value, c.Index, c.ColorSpace, mergeAlpha(c.Alpha, alpha)) return r.parseColorWithAlpha(c.Value, c.Index, c.ColorSpace, mergeAlpha(c.Alpha, alpha))
} }
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
// 入参: segments 渐变分段, alpha 透明度
// 返回: []ColorStop 后端无关的渐变分段
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop {
var gradient []ColorStop
positions := gradientPositions(segments)
for i, segment := range segments {
segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha)
gradient = append(gradient, ColorStop{Offset: positions[i], Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)})
}
return gradient
}
// renderGradientStops 在CMYK分量空间插值,将RGB分段误差控制在半个8位灰阶内 // renderGradientStops 在CMYK分量空间插值,将RGB分段误差控制在半个8位灰阶内
// 入参: segments 原始颜色分段, alpha 对象透明度 // 入参: segments 原始颜色分段, alpha 对象透明度
// 返回: []ColorStop 仅供绘制的颜色节点,不改变原始分段 // 返回: []ColorStop 仅供绘制的颜色节点,不改变原始分段
+31 -31
View File
@@ -57,6 +57,37 @@ type MeshShading struct {
Background color.RGBA Background color.RGBA
} }
// At 按三角形面积权重插值原颜色分量,返回预乘RGBA
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA 采样颜色
func (m *MeshShading) At(x, y float64) color.RGBA {
for i := len(m.Triangles) - 1; i >= 0; i-- {
t := m.Triangles[i]
a, b, c := t[0].Point, t[1].Point, t[2].Point
d := (b.Y-c.Y)*(a.X-c.X) + (c.X-b.X)*(a.Y-c.Y)
if d == 0 {
continue
}
u := ((b.Y-c.Y)*(x-c.X) + (c.X-b.X)*(y-c.Y)) / d
v := ((c.Y-a.Y)*(x-c.X) + (a.X-c.X)*(y-c.Y)) / d
w := 1 - u - v
if u < -1e-12 || v < -1e-12 || w < -1e-12 {
continue
}
var values [4]float64
alpha := 0.0
for j, weight := range [3]float64{u, v, w} {
alpha += weight * t[j].Alpha
for k := range values {
values[k] += weight * t[j].Values[k]
}
}
red, green, blue := meshRGB(t[0].Space, values)
return color.RGBA{meshByte(red * alpha), meshByte(green * alpha), meshByte(blue * alpha), meshByte(alpha)}
}
return m.Background
}
// outline 返回方向一致的三角形轮廓,供绘制裁剪和对象度量共用 // outline 返回方向一致的三角形轮廓,供绘制裁剪和对象度量共用
// 返回: GeometryPath 非零绕数填充轮廓 // 返回: GeometryPath 非零绕数填充轮廓
func (m *MeshShading) outline() GeometryPath { func (m *MeshShading) outline() GeometryPath {
@@ -179,37 +210,6 @@ func meshTriangleIndices(points []ShdPoint, columns, extend int) ([][3]int, erro
return result, nil return result, nil
} }
// At 按三角形面积权重插值原颜色分量,返回预乘RGBA
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA 采样颜色
func (m *MeshShading) At(x, y float64) color.RGBA {
for i := len(m.Triangles) - 1; i >= 0; i-- {
t := m.Triangles[i]
a, b, c := t[0].Point, t[1].Point, t[2].Point
d := (b.Y-c.Y)*(a.X-c.X) + (c.X-b.X)*(a.Y-c.Y)
if d == 0 {
continue
}
u := ((b.Y-c.Y)*(x-c.X) + (c.X-b.X)*(y-c.Y)) / d
v := ((c.Y-a.Y)*(x-c.X) + (a.X-c.X)*(y-c.Y)) / d
w := 1 - u - v
if u < -1e-12 || v < -1e-12 || w < -1e-12 {
continue
}
var values [4]float64
alpha := 0.0
for j, weight := range [3]float64{u, v, w} {
alpha += weight * t[j].Alpha
for k := range values {
values[k] += weight * t[j].Values[k]
}
}
red, green, blue := meshRGB(t[0].Space, values)
return color.RGBA{meshByte(red * alpha), meshByte(green * alpha), meshByte(blue * alpha), meshByte(alpha)}
}
return m.Background
}
// meshRGB 将插值后的颜色分量转换为RGB // meshRGB 将插值后的颜色分量转换为RGB
// 入参: space 颜色模型, values 分量 // 入参: space 颜色模型, values 分量
// 返回: float64 红、绿、蓝分量 // 返回: float64 红、绿、蓝分量
+9 -9
View File
@@ -16,15 +16,6 @@ package ofdgo
import "image/color" import "image/color"
// ColorStop 保存渐变区间位置及预乘RGBA颜色节点
type ColorStop struct {
Offset float64
Color color.RGBA
}
// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变、图案和网格渐变
type PaintKind uint8
const ( const (
PaintNone PaintKind = iota PaintNone PaintKind = iota
PaintSolid PaintSolid
@@ -34,6 +25,15 @@ const (
PaintMesh PaintMesh
) )
// ColorStop 保存渐变区间位置及预乘RGBA颜色节点
type ColorStop struct {
Offset float64
Color color.RGBA
}
// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变、图案和网格渐变
type PaintKind uint8
// Paint 保存后端无关的颜色、渐变和图案,不修改源节点 // Paint 保存后端无关的颜色、渐变和图案,不修改源节点
type Paint struct { type Paint struct {
Kind PaintKind Kind PaintKind
+13 -13
View File
@@ -204,19 +204,6 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
return editor, importer.report, nil return editor, importer.report, nil
} }
// commitObjects 将当前页面待转换对象写入编辑文档
// 返回: error 对象写入错误
func (p *pdfImporter) commitObjects() error {
if len(p.objects) == 0 {
return nil
}
if _, err := p.editor.CopyObjects(p.page, p.objects, 0, 0); err != nil {
return err
}
p.objects = nil
return nil
}
// ConvertPDF 将PDF转换并写入OFD,转换阶段失败时不写入目标 // ConvertPDF 将PDF转换并写入OFD,转换阶段失败时不写入目标
// 入参: ctx 取消上下文, source PDF数据, size 字节数, output OFD输出, options 转换选项 // 入参: ctx 取消上下文, source PDF数据, size 字节数, output OFD输出, options 转换选项
// 返回: PDFImportReport 转换统计, error 错误信息 // 返回: PDFImportReport 转换统计, error 错误信息
@@ -247,6 +234,19 @@ func ConvertPDF(ctx context.Context, source io.ReaderAt, size int64, output io.W
return report, err return report, err
} }
// commitObjects 将当前页面待转换对象写入编辑文档
// 返回: error 对象写入错误
func (p *pdfImporter) commitObjects() error {
if len(p.objects) == 0 {
return nil
}
if _, err := p.editor.CopyObjects(p.page, p.objects, 0, 0); err != nil {
return err
}
p.objects = nil
return nil
}
// pdfPageMatrix 将裁剪框、用户单位及页面旋转转换为OFD毫米坐标 // pdfPageMatrix 将裁剪框、用户单位及页面旋转转换为OFD毫米坐标
// 入参: page PDF页面 // 入参: page PDF页面
// 返回: pdfgo.Matrix 坐标变换, float64 页面宽度, float64 页面高度 // 返回: pdfgo.Matrix 坐标变换, float64 页面宽度, float64 页面高度
+30 -30
View File
@@ -21,6 +21,36 @@ import (
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// pdfMeshVertex 保存局部毫米坐标及原曲面参数
type pdfMeshVertex struct {
point pdfgo.Point
u, v float64
}
// pdfMeshTriangle 保存按输出精度细分的曲面网格
type pdfMeshTriangle struct {
vertices [3]pdfMeshVertex
patch int
}
// pdfMeshKey 区分网格及其输出混合空间
type pdfMeshKey struct {
mesh *pdfgo.MeshGradient
space *pdfgo.ColorSpace
}
// pdfMeshPixel 保存抗锯齿覆盖率与源空间合成分量
type pdfMeshPixel struct {
values [4]float64
shape float64
}
// pdfMeshSample 保存采样点参数,patch为一基编号,零表示未覆盖
type pdfMeshSample struct {
u, v float64
patch int
}
// meshColor 将线性三角网格映射为OFD高洛德渐变,保留顶点和覆盖顺序 // meshColor 将线性三角网格映射为OFD高洛德渐变,保留顶点和覆盖顺序
// 入参: paint PDF网格画刷, box 对象边界 // 入参: paint PDF网格画刷, box 对象边界
// 返回: *FillColor OFD渐变, error 不可线性表达的颜色或坐标错误 // 返回: *FillColor OFD渐变, error 不可线性表达的颜色或坐标错误
@@ -59,36 +89,6 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
return color, nil return color, nil
} }
// pdfMeshVertex 保存局部毫米坐标及原曲面参数
type pdfMeshVertex struct {
point pdfgo.Point
u, v float64
}
// pdfMeshTriangle 保存按输出精度细分的曲面网格
type pdfMeshTriangle struct {
vertices [3]pdfMeshVertex
patch int
}
// pdfMeshKey 区分网格及其输出混合空间
type pdfMeshKey struct {
mesh *pdfgo.MeshGradient
space *pdfgo.ColorSpace
}
// pdfMeshPixel 保存抗锯齿覆盖率与源空间合成分量
type pdfMeshPixel struct {
values [4]float64
shape float64
}
// pdfMeshSample 保存采样点参数,patch为一基编号,零表示未覆盖
type pdfMeshSample struct {
u, v float64
patch int
}
// meshTriangles 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内 // meshTriangles 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内
// 入参: mesh 原始曲面 // 入参: mesh 原始曲面
// 返回: []pdfMeshTriangle 细分结果, error 取消或坐标错误 // 返回: []pdfMeshTriangle 细分结果, error 取消或坐标错误
+19 -19
View File
@@ -21,6 +21,20 @@ import (
"math" "math"
) )
const (
RasterMove RasterVerb = iota
RasterLine
RasterQuad
RasterCubic
RasterClose
)
const (
RasterLinear RasterGradientKind = iota
RasterRadial
RasterMesh
)
// RasterPage 已解析的光栅绘制页面,单位为毫米,页面坐标原点在左上角 // RasterPage 已解析的光栅绘制页面,单位为毫米,页面坐标原点在左上角
// DPI决定像素尺寸和编译时的字形、描边精度,修改DPI后应重新编译 // DPI决定像素尺寸和编译时的字形、描边精度,修改DPI后应重新编译
// 页面及其引用资源在渲染期间只读,不用于重建或保存OFD // 页面及其引用资源在渲染期间只读,不用于重建或保存OFD
@@ -39,14 +53,6 @@ type RasterPoint struct{ X, Y float64 }
// RasterVerb 路径指令 // RasterVerb 路径指令
type RasterVerb uint8 type RasterVerb uint8
const (
RasterMove RasterVerb = iota
RasterLine
RasterQuad
RasterCubic
RasterClose
)
// RasterSegment 路径片段,RasterMove和RasterLine使用End,RasterQuad使用Control1和End // RasterSegment 路径片段,RasterMove和RasterLine使用End,RasterQuad使用Control1和End
type RasterSegment struct { type RasterSegment struct {
Verb RasterVerb Verb RasterVerb
@@ -78,12 +84,6 @@ type RasterPaint struct {
// RasterGradientKind 渐变类型 // RasterGradientKind 渐变类型
type RasterGradientKind uint8 type RasterGradientKind uint8
const (
RasterLinear RasterGradientKind = iota
RasterRadial
RasterMesh
)
// RasterGradient 局部坐标下的轴向、双圆径向或三角网格渐变 // RasterGradient 局部坐标下的轴向、双圆径向或三角网格渐变
// 轴向及径向渐变的Spread为空时延续边界颜色,网格渐变使用Mesh // 轴向及径向渐变的Spread为空时延续边界颜色,网格渐变使用Mesh
type RasterGradient struct { type RasterGradient struct {
@@ -116,11 +116,6 @@ func (p *RasterPage) PixelSize() (int, int, error) {
return int(w), int(h), nil return int(w), int(h), nil
} }
// rasterPositive 判断正有限数
// 入参: v 待检查数值
// 返回: bool 是否为正有限数
func rasterPositive(v float64) bool { return v > 0 && !math.IsInf(v, 0) && !math.IsNaN(v) }
// Apply 将局部点映射到目标坐标 // Apply 将局部点映射到目标坐标
// 入参: p 局部点 // 入参: p 局部点
// 返回: RasterPoint 目标点 // 返回: RasterPoint 目标点
@@ -189,6 +184,11 @@ func (g *RasterGradient) At(x, y float64) color.RGBA {
return g.colorAt(t) return g.colorAt(t)
} }
// rasterPositive 判断正有限数
// 入参: v 待检查数值
// 返回: bool 是否为正有限数
func rasterPositive(v float64) bool { return v > 0 && !math.IsInf(v, 0) && !math.IsNaN(v) }
// rasterRadialDomain 求延伸区间内半径非负的最后一个双圆参数 // rasterRadialDomain 求延伸区间内半径非负的最后一个双圆参数
// 入参: a、b、c 方程系数, radius 起始半径, delta 半径变化, extend 延伸标志 // 入参: a、b、c 方程系数, radius 起始半径, delta 半径变化, extend 延伸标志
// 返回: float64 参数, bool 是否覆盖采样点 // 返回: float64 参数, bool 是否覆盖采样点
+12 -12
View File
@@ -22,6 +22,18 @@ import (
"golang.org/x/image/math/f64" "golang.org/x/image/math/f64"
) )
// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
// 入参: m 局部到页面变换, height 目标像素高度
// 返回: RasterMatrix 局部到像素变换
func (p *RasterPage) PixelTransform(m RasterMatrix, height int) RasterMatrix {
scale := p.DPI / 25.4
for i := range m {
m[i] *= scale
}
m[5] += float64(height) - p.Height*scale
return m
}
// drawRasterImage 按页面变换对图像执行高质量采样 // drawRasterImage 按页面变换对图像执行高质量采样
// 入参: dst 目标图像, src 源图像, m 像素坐标变换 // 入参: dst 目标图像, src 源图像, m 像素坐标变换
func drawRasterImage(dst draw.Image, src image.Image, m RasterMatrix) { func drawRasterImage(dst draw.Image, src image.Image, m RasterMatrix) {
@@ -106,15 +118,3 @@ func rasterImageBounds(source image.Rectangle, m RasterMatrix, target image.Rect
} }
return image.Rect(int(minX), int(minY), int(maxX), int(maxY)) return image.Rect(int(minX), int(minY), int(maxX), int(maxY))
} }
// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
// 入参: m 局部到页面变换, height 目标像素高度
// 返回: RasterMatrix 局部到像素变换
func (p *RasterPage) PixelTransform(m RasterMatrix, height int) RasterMatrix {
scale := p.DPI / 25.4
for i := range m {
m[i] *= scale
}
m[5] += float64(height) - p.Height*scale
return m
}
+6 -6
View File
@@ -56,6 +56,12 @@ type rasterPreparationState struct {
scene RasterScene scene RasterScene
} }
// rasterScene 复用后端无关指令,图片保持只读引用
type rasterScene struct {
backend RasterBackend
page *RasterPage
}
// PreparePage 编译并准备当前配置的独立页面快照,按后端能力复用未修改指令 // PreparePage 编译并准备当前配置的独立页面快照,按后端能力复用未修改指令
// 每次重新解释源页面,按内容使模板、签章、字体及样式派生数据失效 // 每次重新解释源页面,按内容使模板、签章、字体及样式派生数据失效
// 入参: page 源页面 // 入参: page 源页面
@@ -100,12 +106,6 @@ func PrepareRasterScene(backend RasterBackend, page *RasterPage) (RasterScene, e
return &rasterScene{backend: backend, page: cloneRasterPage(page)}, nil return &rasterScene{backend: backend, page: cloneRasterPage(page)}, nil
} }
// rasterScene 复用后端无关指令,图片保持只读引用
type rasterScene struct {
backend RasterBackend
page *RasterPage
}
// Render 绘制固定快照 // Render 绘制固定快照
// 返回: image.Image 独立图像, error 绘制错误 // 返回: image.Image 独立图像, error 绘制错误
func (s *rasterScene) Render() (image.Image, error) { return s.backend.Render(s.page) } func (s *rasterScene) Render() (image.Image, error) { return s.backend.Render(s.page) }
+362 -362
View File
@@ -63,155 +63,6 @@ func (r *Reader) Close() error {
return nil return nil
} }
// initRoot 读取根节点信息
// 返回: error 错误信息
func (r *Reader) initRoot() error {
if err := r.indexPackage(); err != nil {
return err
}
data, err := r.readFile("OFD.xml")
if err != nil {
return fmt.Errorf("failed to read ofd.xml: %w", err)
}
var ofd OFD
if err := xml.Unmarshal(data, &ofd); err != nil {
return fmt.Errorf("failed to unmarshal ofd.xml: %w", err)
}
r.OFD = &ofd
r.ResMap = make(map[string]string)
r.resourcesRead = make(map[string]bool)
r.resourceFiles = make(map[string]string)
r.fontCache = make(map[string]*Font)
r.fontFaces = make(map[string][]*FontFace)
r.colorSpaceCache = make(map[string]*ColorSpace)
r.drawParamCache = make(map[string]*DrawParam)
r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit)
r.pageHeaderCache = make(map[string]PageContent)
return nil
}
// indexPackage 建立包路径索引,拒绝会使显示与验签产生歧义的同名条目
// 返回: error 错误信息
func (r *Reader) indexPackage() error {
count := 0
if r.Zip != nil {
count = len(r.Zip.File)
}
r.fileIndex = make(map[string]*zip.File, count)
r.fileIndexFold = make(map[string]*zip.File, count)
r.fileNamesFold = make(map[string]string, count+len(r.files))
if r.Zip != nil {
for _, f := range r.Zip.File {
if f.FileInfo().IsDir() {
continue
}
name := cleanPackagePath(f.Name)
if !validPackagePath(name) {
return fmt.Errorf("invalid package path: %q", f.Name)
}
fold := strings.ToLower(name)
if previous, ok := r.fileIndexFold[fold]; ok {
return fmt.Errorf("ambiguous package paths: %q and %q", previous.Name, f.Name)
}
r.fileIndex[name] = f
r.fileIndexFold[fold] = f
r.fileNamesFold[fold] = name
}
}
for name := range r.files {
if name != cleanPackagePath(name) || !validPackagePath(name) {
return fmt.Errorf("invalid package path: %q", name)
}
fold := strings.ToLower(name)
if previous, ok := r.fileNamesFold[fold]; ok && previous != name {
return fmt.Errorf("ambiguous package paths: %q and %q", previous, name)
}
r.fileNamesFold[fold] = name
}
return nil
}
// validPackagePath 判断规范化路径是否位于包内
// 入参: name 规范化路径
// 返回: bool 是否有效
func validPackagePath(name string) bool {
return name != "." && name != ".." && !strings.HasPrefix(name, "../") && !strings.HasPrefix(name, "/") && !strings.ContainsAny(name, ":\x00")
}
// readFile 读取文档内的文件
// 入参: name 文件名
// 返回: []byte 文件内容, error 错误信息
func (r *Reader) readFile(name string) ([]byte, error) {
name = cleanPackagePath(name)
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
name = actual
}
if data, ok := r.files[name]; ok {
return bytes.Clone(data), nil
}
if f, ok := r.packageFile(name); ok {
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
return bytes.Clone(data), nil
}
return readZipFile(f)
}
return nil, fmt.Errorf("file not found: %s", name)
}
// openFile 打开文档内的文件流
// 入参: name 文件名
// 返回: io.ReadCloser 文件流, error 错误信息
func (r *Reader) openFile(name string) (io.ReadCloser, error) {
name = cleanPackagePath(name)
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
name = actual
}
if data, ok := r.files[name]; ok {
return io.NopCloser(bytes.NewReader(data)), nil
}
if f, ok := r.packageFile(name); ok {
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
return io.NopCloser(bytes.NewReader(data)), nil
}
return f.Open()
}
return nil, fmt.Errorf("file not found: %s", name)
}
// cleanPackagePath 清理包内文件路径
// 入参: name 文件路径
// 返回: string 清理后的文件路径
func cleanPackagePath(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
name = strings.TrimPrefix(name, "/")
return path.Clean(name)
}
// packageFile 获取包内文件
// 入参: name 文件路径
// 返回: *zip.File 压缩包文件, bool 是否存在
func (r *Reader) packageFile(name string) (*zip.File, bool) {
if f, ok := r.fileIndex[name]; ok {
return f, true
}
if f, ok := r.fileIndexFold[strings.ToLower(name)]; ok {
return f, true
}
return nil, false
}
// readZipFile 读取zip文件内容
// 入参: f zip文件对象
// 返回: []byte 文件内容, error 错误信息
func readZipFile(f *zip.File) ([]byte, error) {
rc, err := f.Open()
if err != nil {
return nil, err
}
defer rc.Close()
return io.ReadAll(rc)
}
// Doc 获取主文档结构 // Doc 获取主文档结构
// 返回: *Document 文档结构, error 错误信息 // 返回: *Document 文档结构, error 错误信息
func (r *Reader) Doc() (*Document, error) { func (r *Reader) Doc() (*Document, error) {
@@ -247,80 +98,6 @@ func (r *Reader) Doc() (*Document, error) {
return r.doc, nil return r.doc, nil
} }
// loadRes 加载资源文件
// 入参: resPath 资源路径
func (r *Reader) loadRes(resPath string) {
if resPath == "" {
return
}
fullPath := r.ResPath(resPath)
if r.resourcesRead[fullPath] {
return
}
data, err := r.readFile(fullPath)
if err != nil {
return
}
var res Res
if err := xml.Unmarshal(data, &res); err != nil {
return
}
baseLoc := res.BaseLoc
for i := range res.ColorSpaces.ColorSpace {
cs := &res.ColorSpaces.ColorSpace[i]
r.colorSpaceCache[cs.ID] = cs
r.resourceFiles[cs.ID] = fullPath
}
for _, mm := range res.MultiMedias.MultiMedia {
if mm.MediaFile != "" {
if finalPath := resolveResourcePath(resPath, baseLoc, mm.MediaFile); finalPath != "" {
r.ResMap[mm.ID] = finalPath
r.resourceFiles[mm.ID] = fullPath
}
}
}
for i := range res.Fonts.Font {
f := &res.Fonts.Font[i]
if f.FontFile != "" {
f.FontFile = resolveResourcePath(resPath, baseLoc, f.FontFile)
}
r.fontCache[f.ID] = f
r.resourceFiles[f.ID] = fullPath
}
for i := range res.DrawParams.DrawParam {
dp := &res.DrawParams.DrawParam[i]
r.drawParamCache[dp.ID] = dp
r.resourceFiles[dp.ID] = fullPath
}
for i := range res.CompositeGraphicUnits.CompositeGraphicUnit {
cgu := &res.CompositeGraphicUnits.CompositeGraphicUnit[i]
r.compositeGraphicUnitCache[cgu.ID] = cgu
r.resourceFiles[cgu.ID] = fullPath
}
r.resourcesRead[fullPath] = true
}
// resolveResourcePath 解析资源文件路径
// 入参: resPath 资源文件路径, baseLoc 资源基准路径, filePath 文件路径
// 返回: string 资源文件路径
func resolveResourcePath(resPath, baseLoc, filePath string) string {
p := strings.TrimSpace(filePath)
if p == "" {
return ""
}
p = strings.ReplaceAll(p, "\\", "/")
if strings.HasPrefix(p, "/") {
return strings.TrimPrefix(path.Clean(p), "/")
}
dir := path.Dir(resPath)
if baseLoc != "" {
if dir != baseLoc {
dir = path.Join(dir, baseLoc)
}
}
return path.Join(dir, p)
}
// PageContent 获取页面内容 // PageContent 获取页面内容
// 每次返回独立的页面内容,修改后可传入Renderer重绘,不会写回原文件 // 每次返回独立的页面内容,修改后可传入Renderer重绘,不会写回原文件
// 入参: page 页面对象 // 入参: page 页面对象
@@ -329,36 +106,6 @@ func (r *Reader) PageContent(page Page) (*PageContent, error) {
return r.readPageContent(page, false) return r.readPageContent(page, false)
} }
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
// 入参: page 页面对象, textOnly 是否仅用于文字提取
// 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) readPageContent(page Page, textOnly bool) (*PageContent, error) {
fullPath := r.ResPath(page.BaseLoc)
data, err := r.readFile(fullPath)
if err != nil {
return nil, err
}
var content PageContent
if textOnly {
decoder := &textPageTokens{Decoder: xml.NewDecoder(bytes.NewReader(data))}
err = xml.NewTokenDecoder(decoder).Decode(&content)
} else {
err = xml.Unmarshal(data, &content)
}
if err != nil {
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
}
content.ID = page.ID
for _, res := range content.PageRes {
r.loadRes(resolveResourcePath(page.BaseLoc, "", res))
}
content.Area, err = r.resolvePageArea(content.Area, content.Template)
if err != nil {
return nil, err
}
return &content, nil
}
// PageCount 获取当前文档总页数 // PageCount 获取当前文档总页数
// 返回: int 页数, error 错误信息 // 返回: int 页数, error 错误信息
func (r *Reader) PageCount() (int, error) { func (r *Reader) PageCount() (int, error) {
@@ -394,100 +141,6 @@ func (r *Reader) PageArea(page Page) (PageArea, error) {
return r.resolvePageArea(content.Area, content.Template) return r.resolvePageArea(content.Area, content.Template)
} }
// loadPageResources 加载页面资源,不解析图元
// 入参: page 页面引用
func (r *Reader) loadPageResources(page Page) {
header, err := r.readPageHeader(page)
if err != nil {
return
}
for _, res := range header.PageRes {
r.loadRes(resolveResourcePath(page.BaseLoc, "", res))
}
}
// readPageHeader 读取页面区域、模板引用和资源声明
// 入参: page 页面对象
// 返回: PageContent 页面头部, error 错误信息
func (r *Reader) readPageHeader(page Page) (PageContent, error) {
name := r.ResPath(page.BaseLoc)
if content, ok := r.pageHeaderCache[name]; ok {
return content, nil
}
f, err := r.openFile(name)
if err != nil {
return PageContent{}, err
}
defer f.Close()
var content PageContent
d := xml.NewDecoder(f)
scan:
for {
token, err := d.Token()
if err == io.EOF {
break
}
if err != nil {
return PageContent{}, err
}
if start, ok := token.(xml.StartElement); ok {
switch start.Name.Local {
case "Page":
continue
case "Template":
var template Template
if err := d.DecodeElement(&template, &start); err != nil {
return PageContent{}, err
}
content.Template = append(content.Template, template)
case "Area":
if err := d.DecodeElement(&content.Area, &start); err != nil {
return PageContent{}, err
}
case "PageRes":
var res string
if err := d.DecodeElement(&res, &start); err != nil {
return PageContent{}, err
}
content.PageRes = append(content.PageRes, res)
case "Content":
break scan
default:
if err := d.Skip(); err != nil {
return PageContent{}, err
}
}
}
}
r.pageHeaderCache[name] = content
return content, nil
}
// resolvePageArea 解析页面区域的继承关系
// 入参: area 页面区域, templates 模板引用
// 返回: PageArea 页面区域, error 错误信息
func (r *Reader) resolvePageArea(area PageArea, templates []Template) (PageArea, error) {
if area != (PageArea{}) || r.doc == nil {
return area, nil
}
for _, ref := range templates {
for _, template := range r.doc.CommonData.TemplatePage {
if template.ID != ref.TemplateID {
continue
}
content, err := r.readPageHeader(Page{BaseLoc: template.BaseLoc})
if err != nil {
return PageArea{}, err
}
if content.Area != (PageArea{}) {
return content.Area, nil
}
break
}
}
return r.doc.CommonData.PageArea, nil
}
// ResPath 获取资源的完整路径 // ResPath 获取资源的完整路径
// 入参: resLink 资源链接 // 入参: resLink 资源链接
// 返回: string 完整路径 // 返回: string 完整路径
@@ -519,21 +172,6 @@ func (r *Reader) ResData(resLink string) ([]byte, error) {
return r.readFile(fullPath) return r.readFile(fullPath)
} }
// readDocumentPart 读取文档关联文件
// 入参: loc 文件位置, value 文档结构
// 返回: string 文件路径, error 错误信息
func (r *Reader) readDocumentPart(loc string, value any) (string, error) {
fullPath := r.ResPath(strings.TrimSpace(loc))
data, err := r.readFile(fullPath)
if err != nil {
return "", err
}
if err := xml.Unmarshal(data, value); err != nil {
return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err)
}
return fullPath, nil
}
// Version 获取OFD版本号 // Version 获取OFD版本号
// 返回: string 版本号 // 返回: string 版本号
func (r *Reader) Version() string { func (r *Reader) Version() string {
@@ -728,3 +366,365 @@ func (r *Reader) Extensions() ([]Extension, error) {
} }
return doc.Extensions.Extension, nil return doc.Extensions.Extension, nil
} }
// initRoot 读取根节点信息
// 返回: error 错误信息
func (r *Reader) initRoot() error {
if err := r.indexPackage(); err != nil {
return err
}
data, err := r.readFile("OFD.xml")
if err != nil {
return fmt.Errorf("failed to read ofd.xml: %w", err)
}
var ofd OFD
if err := xml.Unmarshal(data, &ofd); err != nil {
return fmt.Errorf("failed to unmarshal ofd.xml: %w", err)
}
r.OFD = &ofd
r.ResMap = make(map[string]string)
r.resourcesRead = make(map[string]bool)
r.resourceFiles = make(map[string]string)
r.fontCache = make(map[string]*Font)
r.fontFaces = make(map[string][]*FontFace)
r.colorSpaceCache = make(map[string]*ColorSpace)
r.drawParamCache = make(map[string]*DrawParam)
r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit)
r.pageHeaderCache = make(map[string]PageContent)
return nil
}
// indexPackage 建立包路径索引,拒绝会使显示与验签产生歧义的同名条目
// 返回: error 错误信息
func (r *Reader) indexPackage() error {
count := 0
if r.Zip != nil {
count = len(r.Zip.File)
}
r.fileIndex = make(map[string]*zip.File, count)
r.fileIndexFold = make(map[string]*zip.File, count)
r.fileNamesFold = make(map[string]string, count+len(r.files))
if r.Zip != nil {
for _, f := range r.Zip.File {
if f.FileInfo().IsDir() {
continue
}
name := cleanPackagePath(f.Name)
if !validPackagePath(name) {
return fmt.Errorf("invalid package path: %q", f.Name)
}
fold := strings.ToLower(name)
if previous, ok := r.fileIndexFold[fold]; ok {
return fmt.Errorf("ambiguous package paths: %q and %q", previous.Name, f.Name)
}
r.fileIndex[name] = f
r.fileIndexFold[fold] = f
r.fileNamesFold[fold] = name
}
}
for name := range r.files {
if name != cleanPackagePath(name) || !validPackagePath(name) {
return fmt.Errorf("invalid package path: %q", name)
}
fold := strings.ToLower(name)
if previous, ok := r.fileNamesFold[fold]; ok && previous != name {
return fmt.Errorf("ambiguous package paths: %q and %q", previous, name)
}
r.fileNamesFold[fold] = name
}
return nil
}
// validPackagePath 判断规范化路径是否位于包内
// 入参: name 规范化路径
// 返回: bool 是否有效
func validPackagePath(name string) bool {
return name != "." && name != ".." && !strings.HasPrefix(name, "../") && !strings.HasPrefix(name, "/") && !strings.ContainsAny(name, ":\x00")
}
// readFile 读取文档内的文件
// 入参: name 文件名
// 返回: []byte 文件内容, error 错误信息
func (r *Reader) readFile(name string) ([]byte, error) {
name = cleanPackagePath(name)
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
name = actual
}
if data, ok := r.files[name]; ok {
return bytes.Clone(data), nil
}
if f, ok := r.packageFile(name); ok {
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
return bytes.Clone(data), nil
}
return readZipFile(f)
}
return nil, fmt.Errorf("file not found: %s", name)
}
// openFile 打开文档内的文件流
// 入参: name 文件名
// 返回: io.ReadCloser 文件流, error 错误信息
func (r *Reader) openFile(name string) (io.ReadCloser, error) {
name = cleanPackagePath(name)
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
name = actual
}
if data, ok := r.files[name]; ok {
return io.NopCloser(bytes.NewReader(data)), nil
}
if f, ok := r.packageFile(name); ok {
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
return io.NopCloser(bytes.NewReader(data)), nil
}
return f.Open()
}
return nil, fmt.Errorf("file not found: %s", name)
}
// cleanPackagePath 清理包内文件路径
// 入参: name 文件路径
// 返回: string 清理后的文件路径
func cleanPackagePath(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
name = strings.TrimPrefix(name, "/")
return path.Clean(name)
}
// packageFile 获取包内文件
// 入参: name 文件路径
// 返回: *zip.File 压缩包文件, bool 是否存在
func (r *Reader) packageFile(name string) (*zip.File, bool) {
if f, ok := r.fileIndex[name]; ok {
return f, true
}
if f, ok := r.fileIndexFold[strings.ToLower(name)]; ok {
return f, true
}
return nil, false
}
// readZipFile 读取zip文件内容
// 入参: f zip文件对象
// 返回: []byte 文件内容, error 错误信息
func readZipFile(f *zip.File) ([]byte, error) {
rc, err := f.Open()
if err != nil {
return nil, err
}
defer rc.Close()
return io.ReadAll(rc)
}
// loadRes 加载资源文件
// 入参: resPath 资源路径
func (r *Reader) loadRes(resPath string) {
if resPath == "" {
return
}
fullPath := r.ResPath(resPath)
if r.resourcesRead[fullPath] {
return
}
data, err := r.readFile(fullPath)
if err != nil {
return
}
var res Res
if err := xml.Unmarshal(data, &res); err != nil {
return
}
baseLoc := res.BaseLoc
for i := range res.ColorSpaces.ColorSpace {
cs := &res.ColorSpaces.ColorSpace[i]
r.colorSpaceCache[cs.ID] = cs
r.resourceFiles[cs.ID] = fullPath
}
for _, mm := range res.MultiMedias.MultiMedia {
if mm.MediaFile != "" {
if finalPath := resolveResourcePath(resPath, baseLoc, mm.MediaFile); finalPath != "" {
r.ResMap[mm.ID] = finalPath
r.resourceFiles[mm.ID] = fullPath
}
}
}
for i := range res.Fonts.Font {
f := &res.Fonts.Font[i]
if f.FontFile != "" {
f.FontFile = resolveResourcePath(resPath, baseLoc, f.FontFile)
}
r.fontCache[f.ID] = f
r.resourceFiles[f.ID] = fullPath
}
for i := range res.DrawParams.DrawParam {
dp := &res.DrawParams.DrawParam[i]
r.drawParamCache[dp.ID] = dp
r.resourceFiles[dp.ID] = fullPath
}
for i := range res.CompositeGraphicUnits.CompositeGraphicUnit {
cgu := &res.CompositeGraphicUnits.CompositeGraphicUnit[i]
r.compositeGraphicUnitCache[cgu.ID] = cgu
r.resourceFiles[cgu.ID] = fullPath
}
r.resourcesRead[fullPath] = true
}
// resolveResourcePath 解析资源文件路径
// 入参: resPath 资源文件路径, baseLoc 资源基准路径, filePath 文件路径
// 返回: string 资源文件路径
func resolveResourcePath(resPath, baseLoc, filePath string) string {
p := strings.TrimSpace(filePath)
if p == "" {
return ""
}
p = strings.ReplaceAll(p, "\\", "/")
if strings.HasPrefix(p, "/") {
return strings.TrimPrefix(path.Clean(p), "/")
}
dir := path.Dir(resPath)
if baseLoc != "" {
if dir != baseLoc {
dir = path.Join(dir, baseLoc)
}
}
return path.Join(dir, p)
}
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
// 入参: page 页面对象, textOnly 是否仅用于文字提取
// 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) readPageContent(page Page, textOnly bool) (*PageContent, error) {
fullPath := r.ResPath(page.BaseLoc)
data, err := r.readFile(fullPath)
if err != nil {
return nil, err
}
var content PageContent
if textOnly {
decoder := &textPageTokens{Decoder: xml.NewDecoder(bytes.NewReader(data))}
err = xml.NewTokenDecoder(decoder).Decode(&content)
} else {
err = xml.Unmarshal(data, &content)
}
if err != nil {
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
}
content.ID = page.ID
for _, res := range content.PageRes {
r.loadRes(resolveResourcePath(page.BaseLoc, "", res))
}
content.Area, err = r.resolvePageArea(content.Area, content.Template)
if err != nil {
return nil, err
}
return &content, nil
}
// loadPageResources 加载页面资源,不解析图元
// 入参: page 页面引用
func (r *Reader) loadPageResources(page Page) {
header, err := r.readPageHeader(page)
if err != nil {
return
}
for _, res := range header.PageRes {
r.loadRes(resolveResourcePath(page.BaseLoc, "", res))
}
}
// readPageHeader 读取页面区域、模板引用和资源声明
// 入参: page 页面对象
// 返回: PageContent 页面头部, error 错误信息
func (r *Reader) readPageHeader(page Page) (PageContent, error) {
name := r.ResPath(page.BaseLoc)
if content, ok := r.pageHeaderCache[name]; ok {
return content, nil
}
f, err := r.openFile(name)
if err != nil {
return PageContent{}, err
}
defer f.Close()
var content PageContent
d := xml.NewDecoder(f)
scan:
for {
token, err := d.Token()
if err == io.EOF {
break
}
if err != nil {
return PageContent{}, err
}
if start, ok := token.(xml.StartElement); ok {
switch start.Name.Local {
case "Page":
continue
case "Template":
var template Template
if err := d.DecodeElement(&template, &start); err != nil {
return PageContent{}, err
}
content.Template = append(content.Template, template)
case "Area":
if err := d.DecodeElement(&content.Area, &start); err != nil {
return PageContent{}, err
}
case "PageRes":
var res string
if err := d.DecodeElement(&res, &start); err != nil {
return PageContent{}, err
}
content.PageRes = append(content.PageRes, res)
case "Content":
break scan
default:
if err := d.Skip(); err != nil {
return PageContent{}, err
}
}
}
}
r.pageHeaderCache[name] = content
return content, nil
}
// resolvePageArea 解析页面区域的继承关系
// 入参: area 页面区域, templates 模板引用
// 返回: PageArea 页面区域, error 错误信息
func (r *Reader) resolvePageArea(area PageArea, templates []Template) (PageArea, error) {
if area != (PageArea{}) || r.doc == nil {
return area, nil
}
for _, ref := range templates {
for _, template := range r.doc.CommonData.TemplatePage {
if template.ID != ref.TemplateID {
continue
}
content, err := r.readPageHeader(Page{BaseLoc: template.BaseLoc})
if err != nil {
return PageArea{}, err
}
if content.Area != (PageArea{}) {
return content.Area, nil
}
break
}
}
return r.doc.CommonData.PageArea, nil
}
// readDocumentPart 读取文档关联文件
// 入参: loc 文件位置, value 文档结构
// 返回: string 文件路径, error 错误信息
func (r *Reader) readDocumentPart(loc string, value any) (string, error) {
fullPath := r.ResPath(strings.TrimSpace(loc))
data, err := r.readFile(fullPath)
if err != nil {
return "", err
}
if err := xml.Unmarshal(data, value); err != nil {
return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err)
}
return fullPath, nil
}
+6 -6
View File
@@ -16,6 +16,12 @@ package ofdgo
import "fmt" import "fmt"
// ObjectContour 页面坐标中的填充轮廓,Path为SVG路径,EvenOdd表示奇偶填充
type ObjectContour struct {
Path string `json:"path"`
EvenOdd bool `json:"evenOdd,omitempty"`
}
// ObjectBounds 获取文字、路径、图片或复合对象在页面坐标中的轴对齐范围,不修改对象 // ObjectBounds 获取文字、路径、图片或复合对象在页面坐标中的轴对齐范围,不修改对象
// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素 // 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素
// 底纹按填充或描边轮廓度量,不展开图案单元 // 底纹按填充或描边轮廓度量,不展开图案单元
@@ -27,12 +33,6 @@ func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, er
return result.Bounds, err return result.Bounds, err
} }
// ObjectContour 页面坐标中的填充轮廓,Path为SVG路径,EvenOdd表示奇偶填充
type ObjectContour struct {
Path string `json:"path"`
EvenOdd bool `json:"evenOdd,omitempty"`
}
// AnnotationGeometry 获取注解外观的实际内容轮廓,不将Appearance容器边界视为绘制内容 // AnnotationGeometry 获取注解外观的实际内容轮廓,不将Appearance容器边界视为绘制内容
// 链接保留透明对象定义的交互区域,图片按几何范围度量而不解码透明像素 // 链接保留透明对象定义的交互区域,图片按几何范围度量而不解码透明像素
// 入参: annotation 注解 // 入参: annotation 注解
+15 -15
View File
@@ -32,15 +32,10 @@ type semanticCompiler struct {
patterns *renderCache[patternCellKey, []RasterCommand] patterns *renderCache[patternCellKey, []RasterCommand]
} }
// newSemanticCompiler 绑定当前字体和几何能力 // semanticStyle 保存已继承的路径颜色和描边参数
// 入参: r 渲染器 type semanticStyle struct {
// 返回: *semanticCompiler 编译器, error 能力错误 fill, stroke *FillColor
func newSemanticCompiler(r *Renderer) (*semanticCompiler, error) { options StrokeOptions
geometry, err := r.Geometry()
if err != nil {
return nil, err
}
return &semanticCompiler{renderer: r, geometry: geometry, patterns: &renderCache[patternCellKey, []RasterCommand]{limit: 8 << 20}}, nil
} }
// DrawObject 解释叶子对象,不调用任何具体页面编译器 // DrawObject 解释叶子对象,不调用任何具体页面编译器
@@ -64,6 +59,17 @@ func (c *semanticCompiler) DrawObject(object *GraphicObject, state RenderState)
return nil return nil
} }
// newSemanticCompiler 绑定当前字体和几何能力
// 入参: r 渲染器
// 返回: *semanticCompiler 编译器, error 能力错误
func newSemanticCompiler(r *Renderer) (*semanticCompiler, error) {
geometry, err := r.Geometry()
if err != nil {
return nil, err
}
return &semanticCompiler{renderer: r, geometry: geometry, patterns: &renderCache[patternCellKey, []RasterCommand]{limit: 8 << 20}}, nil
}
// segments 转换光栅曲线,仅在输出边界展开椭圆弧 // segments 转换光栅曲线,仅在输出边界展开椭圆弧
// 入参: path 页面路径 // 入参: path 页面路径
// 返回: []RasterSegment 光栅路径, error 能力或路径错误 // 返回: []RasterSegment 光栅路径, error 能力或路径错误
@@ -268,12 +274,6 @@ func semanticGradient(paint Paint, matrix Matrix) (*RasterGradient, Matrix) {
return g, matrix return g, matrix
} }
// semanticStyle 保存已继承的路径颜色和描边参数
type semanticStyle struct {
fill, stroke *FillColor
options StrokeOptions
}
// objectStyle 合并继承与对象样式,不修改共享绘制参数 // objectStyle 合并继承与对象样式,不修改共享绘制参数
// 入参: object 路径样式, defaults 继承样式, linear 组合变换 // 入参: object 路径样式, defaults 继承样式, linear 组合变换
// 返回: semanticStyle 解析样式 // 返回: semanticStyle 解析样式
+7 -7
View File
@@ -22,6 +22,13 @@ import (
"slices" "slices"
) )
// DrawStamp 保留签章图片和嵌套OFD的绘制顺序,不混入正文搜索
// 入参: stamp 印章
// 返回: error 印章解析或编译错误
func (c *semanticCompiler) DrawStamp(stamp Stamp) error {
return c.sharedStamp(stamp, (*semanticCompiler).drawStampContent)
}
// imageObject 共用图片定位、裁剪和边框,不在度量时读取像素 // imageObject 共用图片定位、裁剪和边框,不在度量时读取像素
// 入参: object 图片对象, state 继承状态 // 入参: object 图片对象, state 继承状态
// 返回: error 资源或几何错误 // 返回: error 资源或几何错误
@@ -135,13 +142,6 @@ func (c *semanticCompiler) addImage(img image.Image, matrix Matrix, clip *Geomet
return nil return nil
} }
// DrawStamp 保留签章图片和嵌套OFD的绘制顺序,不混入正文搜索
// 入参: stamp 印章
// 返回: error 印章解析或编译错误
func (c *semanticCompiler) DrawStamp(stamp Stamp) error {
return c.sharedStamp(stamp, (*semanticCompiler).drawStampContent)
}
// drawStampContent 解释未缓存的签章内容,保持嵌套文档及裁剪语义 // drawStampContent 解释未缓存的签章内容,保持嵌套文档及裁剪语义
// 入参: stamp 印章 // 入参: stamp 印章
// 返回: error 印章解析或编译错误 // 返回: error 印章解析或编译错误
+59 -59
View File
@@ -87,30 +87,6 @@ func (r *Renderer) RenderTo(page *PageContent, writer io.Writer, format string)
return render(page, writer) return render(page, writer)
} }
// outputRenderer 获取格式扩展名和渲染方法
// 入参: format 输出格式
// 返回: string 扩展名, func 渲染方法, error 错误信息
func (r *Renderer) outputRenderer(format string) (string, func(*PageContent, io.Writer) error, error) {
switch strings.ToLower(strings.TrimSpace(format)) {
case "svg":
return "svg", r.RenderToSVG, nil
case "pdf":
return "pdf", r.RenderToPDF, nil
case "txt":
return "txt", r.RenderToText, nil
case "eps":
return "eps", r.RenderToEPS, nil
case "png":
return "png", r.RenderToPNG, nil
case "jpg", "jpeg":
return "jpg", func(page *PageContent, writer io.Writer) error {
return r.RenderToJPEG(page, writer, &jpeg.Options{Quality: 95})
}, nil
default:
return "", nil, fmt.Errorf("unsupported output format %s", format)
}
}
// RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出 // RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出
// 入参: writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt, indices 零基页面索引,省略则全部,按原页序去重 // 入参: writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt, indices 零基页面索引,省略则全部,按原页序去重
// 返回: error 错误信息 // 返回: error 错误信息
@@ -203,16 +179,6 @@ func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error
return nil return nil
} }
// exportProgress 回报导出进度并传递调用方的停止原因
// 入参: completed 已处理页数, total 总页数
// 返回: error 停止原因
func (r *Renderer) exportProgress(completed, total int) error {
if r.OnExportProgress != nil {
return r.OnExportProgress(completed, total)
}
return nil
}
// RenderToImage 渲染为光栅图 // RenderToImage 渲染为光栅图
// 入参: page 页面内容 // 入参: page 页面内容
// 返回: image.Image 图像对象, error 错误信息 // 返回: image.Image 图像对象, error 错误信息
@@ -257,31 +223,6 @@ func (r *Renderer) RenderToJPEG(page *PageContent, writer io.Writer, options *jp
return jpeg.Encode(&imageMetadataWriter{writer: writer, skip: 2, data: segment}, fillWhiteBackground(img), options) return jpeg.Encode(&imageMetadataWriter{writer: writer, skip: 2, data: segment}, fillWhiteBackground(img), options)
} }
// fillWhiteBackground 填充白色背景,复用当前导出图片的RGBA像素
// 入参: img 图片对象
// 返回: image.Image 图片对象
func fillWhiteBackground(img image.Image) image.Image {
bounds := img.Bounds()
if rgba, ok := img.(*image.RGBA); ok {
for y := 0; y < bounds.Dy(); y++ {
row := rgba.Pix[y*rgba.Stride : y*rgba.Stride+bounds.Dx()*4]
for i := 0; i < len(row); i += 4 {
if alpha := 255 - row[i+3]; alpha != 0 {
row[i] += alpha
row[i+1] += alpha
row[i+2] += alpha
row[i+3] = 255
}
}
}
return rgba
}
dst := image.NewRGBA(bounds)
draw.Draw(dst, bounds, image.White, image.Point{}, draw.Src)
draw.Draw(dst, bounds, img, bounds.Min, draw.Over)
return dst
}
// RenderToSVG 渲染为SVG // RenderToSVG 渲染为SVG
// 入参: page 页面内容, writer 输出流 // 入参: page 页面内容, writer 输出流
// 返回: error 错误信息 // 返回: error 错误信息
@@ -363,6 +304,65 @@ func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) e
return r.renderPDFPages(pages, writer, r.OnExportProgress) return r.renderPDFPages(pages, writer, r.OnExportProgress)
} }
// outputRenderer 获取格式扩展名和渲染方法
// 入参: format 输出格式
// 返回: string 扩展名, func 渲染方法, error 错误信息
func (r *Renderer) outputRenderer(format string) (string, func(*PageContent, io.Writer) error, error) {
switch strings.ToLower(strings.TrimSpace(format)) {
case "svg":
return "svg", r.RenderToSVG, nil
case "pdf":
return "pdf", r.RenderToPDF, nil
case "txt":
return "txt", r.RenderToText, nil
case "eps":
return "eps", r.RenderToEPS, nil
case "png":
return "png", r.RenderToPNG, nil
case "jpg", "jpeg":
return "jpg", func(page *PageContent, writer io.Writer) error {
return r.RenderToJPEG(page, writer, &jpeg.Options{Quality: 95})
}, nil
default:
return "", nil, fmt.Errorf("unsupported output format %s", format)
}
}
// exportProgress 回报导出进度并传递调用方的停止原因
// 入参: completed 已处理页数, total 总页数
// 返回: error 停止原因
func (r *Renderer) exportProgress(completed, total int) error {
if r.OnExportProgress != nil {
return r.OnExportProgress(completed, total)
}
return nil
}
// fillWhiteBackground 填充白色背景,复用当前导出图片的RGBA像素
// 入参: img 图片对象
// 返回: image.Image 图片对象
func fillWhiteBackground(img image.Image) image.Image {
bounds := img.Bounds()
if rgba, ok := img.(*image.RGBA); ok {
for y := 0; y < bounds.Dy(); y++ {
row := rgba.Pix[y*rgba.Stride : y*rgba.Stride+bounds.Dx()*4]
for i := 0; i < len(row); i += 4 {
if alpha := 255 - row[i+3]; alpha != 0 {
row[i] += alpha
row[i+1] += alpha
row[i+2] += alpha
row[i+3] = 255
}
}
}
return rgba
}
dst := image.NewRGBA(bounds)
draw.Draw(dst, bounds, image.White, image.Point{}, draw.Src)
draw.Draw(dst, bounds, img, bounds.Min, draw.Over)
return dst
}
// renderPDFPages 将已解析页面交给指定PDF后端 // renderPDFPages 将已解析页面交给指定PDF后端
// 入参: pages 页面列表, writer 输出流, progress 页面处理进度 // 入参: pages 页面列表, writer 输出流, progress 页面处理进度
// 返回: error 错误信息 // 返回: error 错误信息
+9 -9
View File
@@ -39,6 +39,15 @@ type stampContentState struct {
// stampContentStateKey 隔离渲染器私有的共享签章数据 // stampContentStateKey 隔离渲染器私有的共享签章数据
type stampContentStateKey struct{} type stampContentStateKey struct{}
// patternCellKey 区分共享底纹、颜色、透明度和影响单元语义的页面变换
type patternCellKey struct {
pattern *Pattern
color, space string
index, alpha int
colorAlpha int
matrix [6]float64
}
// resetFonts 使包含文字的签章编译结果随字体配置失效 // resetFonts 使包含文字的签章编译结果随字体配置失效
func (s *stampContentState) resetFonts() { func (s *stampContentState) resetFonts() {
s.cache = renderCache[stampContentKey, []RasterCommand]{limit: 16 << 20} s.cache = renderCache[stampContentKey, []RasterCommand]{limit: 16 << 20}
@@ -88,15 +97,6 @@ func (c *semanticCompiler) sharedStamp(stamp Stamp, compile func(*semanticCompil
return nil return nil
} }
// patternCellKey 区分共享底纹、颜色、透明度和影响单元语义的页面变换
type patternCellKey struct {
pattern *Pattern
color, space string
index, alpha int
colorAlpha int
matrix [6]float64
}
// patternCell 复用同次编译中相同底纹单元,不缓存外部对象的裁剪 // patternCell 复用同次编译中相同底纹单元,不缓存外部对象的裁剪
// 入参: pattern 底纹画刷, matrix 单元到页面矩阵 // 入参: pattern 底纹画刷, matrix 单元到页面矩阵
// 返回: []RasterCommand 只读单元指令, error 编译错误 // 返回: []RasterCommand 只读单元指令, error 编译错误
+21 -21
View File
@@ -14,6 +14,15 @@
package ofdgo package ofdgo
const (
sceneObject sceneCommandKind = iota
sceneStamp
sceneBeginObject
sceneBeginAnnotation
sceneEnd
sceneSkip
)
// PageScene 保存后端无关的页面语义与绘制顺序,不提前展开文字或重新编码图片 // PageScene 保存后端无关的页面语义与绘制顺序,不提前展开文字或重新编码图片
// 场景借用源页面及资源的只读引用,编辑文档后应重新构建 // 场景借用源页面及资源的只读引用,编辑文档后应重新构建
type PageScene struct { type PageScene struct {
@@ -35,14 +44,8 @@ type sceneCommand struct {
// sceneCommandKind 区分场景中的绘制和分组事件 // sceneCommandKind 区分场景中的绘制和分组事件
type sceneCommandKind uint8 type sceneCommandKind uint8
const ( // sceneRecorder 收集公共遍历事件,不解释绘制外观
sceneObject sceneCommandKind = iota type sceneRecorder PageScene
sceneStamp
sceneBeginObject
sceneBeginAnnotation
sceneEnd
sceneSkip
)
// CompileScene 展开页面结构,保留原文、字形索引、动作和对象标识 // CompileScene 展开页面结构,保留原文、字形索引、动作和对象标识
// 不提前绘制文字或图片,裁剪轮廓按几何后端需求解析 // 不提前绘制文字或图片,裁剪轮廓按几何后端需求解析
@@ -102,19 +105,6 @@ func (s *PageScene) Walk(visitor PageVisitor) error {
return nil return nil
} }
// closeSceneGroups 在访问器失败时配对结束已建立的分组
// 入参: groups 分组访问器, stack 分组状态
func closeSceneGroups(groups PageGroups, stack []bool) {
for i := len(stack) - 1; i >= 0; i-- {
if stack[i] {
groups.EndObject()
}
}
}
// sceneRecorder 收集公共遍历事件,不解释绘制外观
type sceneRecorder PageScene
// DrawObject 保留源对象及继承状态 // DrawObject 保留源对象及继承状态
// 入参: object 源对象, state 继承状态 // 入参: object 源对象, state 继承状态
// 返回: error 固定为空 // 返回: error 固定为空
@@ -155,3 +145,13 @@ func (s *sceneRecorder) EndObject() { s.commands = append(s.commands, sceneComma
func (s *sceneRecorder) SkipObjects(count int) { func (s *sceneRecorder) SkipObjects(count int) {
s.commands = append(s.commands, sceneCommand{kind: sceneSkip, count: count}) s.commands = append(s.commands, sceneCommand{kind: sceneSkip, count: count})
} }
// closeSceneGroups 在访问器失败时配对结束已建立的分组
// 入参: groups 分组访问器, stack 分组状态
func closeSceneGroups(groups PageGroups, stack []bool) {
for i := len(stack) - 1; i >= 0; i-- {
if stack[i] {
groups.EndObject()
}
}
}
+154 -154
View File
@@ -44,160 +44,6 @@ type glyphOutlineKey struct {
size float64 size float64
} }
// preparedMetrics 复用已包装字体的后端度量,不混用不同后端实例
// 入参: prepared 已包装字体
// 返回: FontMetrics 字体度量, error 字体解析错误
func (r *Renderer) preparedMetrics(prepared *PreparedFont) (FontMetrics, error) {
if prepared.digest == ([32]byte{}) {
prepared.digest = sha256.Sum256(prepared.Data)
}
key := prepared.digest
if metrics, ok := r.fontMetrics.get(key); ok {
return metrics, nil
}
metrics, err := r.backends.Fonts.OpenFont(prepared.Data)
if err != nil {
return nil, err
}
r.fontMetrics.put(key, metrics, len(prepared.Data)*2+256)
return metrics, nil
}
// preparedOutline 按字形和字号复用只读轮廓
// 入参: prepared 字体, metrics 度量, glyph 字形编号, size 毫米字号
// 返回: GeometryPath 字形路径, error 能力或解析错误
func (r *Renderer) preparedOutline(prepared *PreparedFont, metrics FontMetrics, glyph uint16, size float64) (GeometryPath, error) {
key := glyphOutlineKey{prepared, glyph, size}
if path, ok := r.glyphOutlines.get(key); ok {
return path, nil
}
provider, ok := metrics.(FontOutlines)
if !ok {
return nil, fmt.Errorf("font outlines: %w", ErrBackendUnavailable)
}
path, err := provider.GlyphOutline(glyph, size)
if err == nil {
r.glyphOutlines.put(key, path, len(path)*96+128)
}
return path, err
}
// preparedOutlines 批量提取缓存未命中的字形,保留原始编号顺序,不触发塑形
// 入参: prepared 字体, metrics 度量, glyphs 字形编号, size 毫米字号
// 返回: []GeometryPath 同序只读轮廓, error 能力或解析错误
func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics, glyphs []uint16, size float64) ([]GeometryPath, error) {
result := make([]GeometryPath, len(glyphs))
var indices []int
missing := make([]uint16, 0)
unique := make(map[uint16]int)
for i, glyph := range glyphs {
if path, ok := r.glyphOutlines.get(glyphOutlineKey{prepared, glyph, size}); ok {
result[i] = path
continue
}
if indices == nil {
indices = make([]int, len(glyphs))
for j := range indices {
indices[j] = -1
}
}
index, ok := unique[glyph]
if !ok {
index = len(missing)
unique[glyph] = index
missing = append(missing, glyph)
}
indices[i] = index
}
if len(missing) == 0 {
return result, nil
}
var paths []GeometryPath
if batch, ok := metrics.(FontOutlineBatch); ok {
var err error
paths, err = batch.GlyphOutlines(missing, size)
if err != nil {
return nil, err
}
if len(paths) != len(missing) {
return nil, fmt.Errorf("font outline batch returned %d paths for %d glyphs", len(paths), len(missing))
}
} else {
provider, ok := metrics.(FontOutlines)
if !ok {
return nil, fmt.Errorf("font outlines: %w", ErrBackendUnavailable)
}
paths = make([]GeometryPath, len(missing))
for i, glyph := range missing {
var err error
paths[i], err = provider.GlyphOutline(glyph, size)
if err != nil {
return nil, err
}
}
}
for i, path := range paths {
r.glyphOutlines.put(glyphOutlineKey{prepared, missing[i], size}, path, len(path)*96+128)
}
for i, index := range indices {
if index >= 0 {
result[i] = paths[index]
}
}
return result, nil
}
// renderObjectMatrix 统一边界、局部变换与父变换的组合顺序
// 入参: boundary 对象边界, local 局部矩阵, state 继承状态
// 返回: Matrix 页面矩阵, Matrix 不含边界平移的组合矩阵
func renderObjectMatrix(boundary string, local Matrix, state RenderState) (Matrix, Matrix) {
box, _ := ParseBox(boundary)
linear := local
if state.Parent != nil {
linear = state.Parent.Multiply(local)
}
matrix := TranslationMatrix(box.X, box.Y).Multiply(linear)
if state.BoundaryInCTM && state.Parent != nil {
matrix = state.Parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(local)
}
return matrix, linear
}
// objectGeometryClip 解析对象裁剪并合并父裁剪
// 入参: clips 对象裁剪, boundary 边界, local 局部矩阵, state 继承状态
// 返回: *GeometryPath 页面裁剪, error 几何错误
func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matrix, state RenderState) (*GeometryPath, error) {
if clips == nil {
return state.Clip, nil
}
matrix, _ := renderObjectMatrix(boundary, local, state)
if clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips
copy.TransFlag = nil
clips = &copy
box, _ := ParseBox(boundary)
matrix = TranslationMatrix(box.X, box.Y)
if state.BoundaryInCTM && state.Parent != nil {
matrix = state.Parent.Multiply(matrix)
}
}
geometry, err := r.Geometry()
if err != nil {
return nil, err
}
return geometry.Clip(r, clips, matrix, state.Clip)
}
// clipGeometry 将非零填充路径与页面裁剪相交
// 入参: geometry 几何后端, path 页面路径, clip 页面裁剪
// 返回: GeometryPath 裁剪路径, error 几何错误
func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPath) (GeometryPath, error) {
if clip == nil {
return path, nil
}
return geometry.Combine(path, *clip, GeometryIntersect)
}
// PositionText 定位文字、字形与搜索范围,不依赖页面输出后端 // PositionText 定位文字、字形与搜索范围,不依赖页面输出后端
// 入参: object 文字对象, state 继承状态 // 入参: object 文字对象, state 继承状态
// 返回: *PositionedText 定位结果, error 字体或几何错误 // 返回: *PositionedText 定位结果, error 字体或几何错误
@@ -403,6 +249,160 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
return result, nil return result, nil
} }
// preparedMetrics 复用已包装字体的后端度量,不混用不同后端实例
// 入参: prepared 已包装字体
// 返回: FontMetrics 字体度量, error 字体解析错误
func (r *Renderer) preparedMetrics(prepared *PreparedFont) (FontMetrics, error) {
if prepared.digest == ([32]byte{}) {
prepared.digest = sha256.Sum256(prepared.Data)
}
key := prepared.digest
if metrics, ok := r.fontMetrics.get(key); ok {
return metrics, nil
}
metrics, err := r.backends.Fonts.OpenFont(prepared.Data)
if err != nil {
return nil, err
}
r.fontMetrics.put(key, metrics, len(prepared.Data)*2+256)
return metrics, nil
}
// preparedOutline 按字形和字号复用只读轮廓
// 入参: prepared 字体, metrics 度量, glyph 字形编号, size 毫米字号
// 返回: GeometryPath 字形路径, error 能力或解析错误
func (r *Renderer) preparedOutline(prepared *PreparedFont, metrics FontMetrics, glyph uint16, size float64) (GeometryPath, error) {
key := glyphOutlineKey{prepared, glyph, size}
if path, ok := r.glyphOutlines.get(key); ok {
return path, nil
}
provider, ok := metrics.(FontOutlines)
if !ok {
return nil, fmt.Errorf("font outlines: %w", ErrBackendUnavailable)
}
path, err := provider.GlyphOutline(glyph, size)
if err == nil {
r.glyphOutlines.put(key, path, len(path)*96+128)
}
return path, err
}
// preparedOutlines 批量提取缓存未命中的字形,保留原始编号顺序,不触发塑形
// 入参: prepared 字体, metrics 度量, glyphs 字形编号, size 毫米字号
// 返回: []GeometryPath 同序只读轮廓, error 能力或解析错误
func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics, glyphs []uint16, size float64) ([]GeometryPath, error) {
result := make([]GeometryPath, len(glyphs))
var indices []int
missing := make([]uint16, 0)
unique := make(map[uint16]int)
for i, glyph := range glyphs {
if path, ok := r.glyphOutlines.get(glyphOutlineKey{prepared, glyph, size}); ok {
result[i] = path
continue
}
if indices == nil {
indices = make([]int, len(glyphs))
for j := range indices {
indices[j] = -1
}
}
index, ok := unique[glyph]
if !ok {
index = len(missing)
unique[glyph] = index
missing = append(missing, glyph)
}
indices[i] = index
}
if len(missing) == 0 {
return result, nil
}
var paths []GeometryPath
if batch, ok := metrics.(FontOutlineBatch); ok {
var err error
paths, err = batch.GlyphOutlines(missing, size)
if err != nil {
return nil, err
}
if len(paths) != len(missing) {
return nil, fmt.Errorf("font outline batch returned %d paths for %d glyphs", len(paths), len(missing))
}
} else {
provider, ok := metrics.(FontOutlines)
if !ok {
return nil, fmt.Errorf("font outlines: %w", ErrBackendUnavailable)
}
paths = make([]GeometryPath, len(missing))
for i, glyph := range missing {
var err error
paths[i], err = provider.GlyphOutline(glyph, size)
if err != nil {
return nil, err
}
}
}
for i, path := range paths {
r.glyphOutlines.put(glyphOutlineKey{prepared, missing[i], size}, path, len(path)*96+128)
}
for i, index := range indices {
if index >= 0 {
result[i] = paths[index]
}
}
return result, nil
}
// renderObjectMatrix 统一边界、局部变换与父变换的组合顺序
// 入参: boundary 对象边界, local 局部矩阵, state 继承状态
// 返回: Matrix 页面矩阵, Matrix 不含边界平移的组合矩阵
func renderObjectMatrix(boundary string, local Matrix, state RenderState) (Matrix, Matrix) {
box, _ := ParseBox(boundary)
linear := local
if state.Parent != nil {
linear = state.Parent.Multiply(local)
}
matrix := TranslationMatrix(box.X, box.Y).Multiply(linear)
if state.BoundaryInCTM && state.Parent != nil {
matrix = state.Parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(local)
}
return matrix, linear
}
// objectGeometryClip 解析对象裁剪并合并父裁剪
// 入参: clips 对象裁剪, boundary 边界, local 局部矩阵, state 继承状态
// 返回: *GeometryPath 页面裁剪, error 几何错误
func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matrix, state RenderState) (*GeometryPath, error) {
if clips == nil {
return state.Clip, nil
}
matrix, _ := renderObjectMatrix(boundary, local, state)
if clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips
copy.TransFlag = nil
clips = &copy
box, _ := ParseBox(boundary)
matrix = TranslationMatrix(box.X, box.Y)
if state.BoundaryInCTM && state.Parent != nil {
matrix = state.Parent.Multiply(matrix)
}
}
geometry, err := r.Geometry()
if err != nil {
return nil, err
}
return geometry.Clip(r, clips, matrix, state.Clip)
}
// clipGeometry 将非零填充路径与页面裁剪相交
// 入参: geometry 几何后端, path 页面路径, clip 页面裁剪
// 返回: GeometryPath 裁剪路径, error 几何错误
func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPath) (GeometryPath, error) {
if clip == nil {
return path, nil
}
return geometry.Combine(path, *clip, GeometryIntersect)
}
// addGeometrySpan 以公共几何语义收集文字选择区域,保留旋转和斜切 // addGeometrySpan 以公共几何语义收集文字选择区域,保留旋转和斜切
// 入参: geometry 几何后端, start 起始字符, end 结束字符, bounds 局部范围, matrix 页面变换, clip 页面裁剪 // 入参: geometry 几何后端, start 起始字符, end 结束字符, bounds 局部范围, matrix 页面变换, clip 页面裁剪
// 返回: error 几何错误 // 返回: error 几何错误
+7 -7
View File
@@ -90,6 +90,13 @@ func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
return nil return nil
} }
// WalkObject 访问单个对象或展开复合图元,保留父级裁剪和源对象标识
// 入参: object 源对象, state 继承状态, visitor 页面访问器
// 返回: error 遍历或访问器错误
func (r *Renderer) WalkObject(object *GraphicObject, state RenderState, visitor PageVisitor) error {
return r.walkObject(object, state, visitor, nil)
}
// walkAnnotation 访问注解外观并保持独立分组和定位 // walkAnnotation 访问注解外观并保持独立分组和定位
// 入参: annotation 注解, visitor 页面访问器 // 入参: annotation 注解, visitor 页面访问器
// 返回: error 遍历或访问器错误 // 返回: error 遍历或访问器错误
@@ -149,13 +156,6 @@ func (r *Renderer) walkLayers(layers []Layer, order int, visitor PageVisitor) er
return nil return nil
} }
// WalkObject 访问单个对象或展开复合图元,保留父级裁剪和源对象标识
// 入参: object 源对象, state 继承状态, visitor 页面访问器
// 返回: error 遍历或访问器错误
func (r *Renderer) WalkObject(object *GraphicObject, state RenderState, visitor PageVisitor) error {
return r.walkObject(object, state, visitor, nil)
}
// walkObject 访问对象并传递当前资源引用链 // walkObject 访问对象并传递当前资源引用链
// 入参: object 源对象, state 继承状态, visitor 页面访问器, references 当前资源引用链 // 入参: object 源对象, state 继承状态, visitor 页面访问器, references 当前资源引用链
// 返回: error 遍历或访问器错误 // 返回: error 遍历或访问器错误
+22 -22
View File
@@ -30,18 +30,16 @@ type CanvasBackend struct {
ColorSpace canvas.ColorSpace ColorSpace canvas.ColorSpace
} }
// init 注册Canvas适配器和默认组合 // rasterStrokeGradientCanvas 保留页面坐标描边的局部渐变定位
func init() { type rasterStrokeGradientCanvas struct {
if err := RegisterRenderBackend("canvas", defaultRenderBackends); err != nil { rasterGradientCanvas
panic(err) inverse RasterMatrix
}
} }
// defaultRenderBackends 创建默认组合,调用方可按职责替换任意后端 // rasterGradientCanvas 将公共渐变交给Canvas采样,不改变OFD周期语义
// 返回: RenderBackends 默认后端组合 type rasterGradientCanvas struct {
func defaultRenderBackends() RenderBackends { gradient *RasterGradient
backend := CanvasBackend{} space canvas.ColorSpace
return RenderBackends{FontResources: SFNTBackend{}, Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
} }
// Name 返回后端标识 // Name 返回后端标识
@@ -182,12 +180,6 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
return img, nil return img, nil
} }
// rasterStrokeGradientCanvas 保留页面坐标描边的局部渐变定位
type rasterStrokeGradientCanvas struct {
rasterGradientCanvas
inverse RasterMatrix
}
// At 返回原局部坐标的描边渐变颜色 // At 返回原局部坐标的描边渐变颜色
// 入参: x 页面横坐标, y 页面纵坐标 // 入参: x 页面横坐标, y 页面纵坐标
// 返回: color.RGBA 预乘颜色 // 返回: color.RGBA 预乘颜色
@@ -204,12 +196,6 @@ func (g rasterStrokeGradientCanvas) SetColorSpace(space canvas.ColorSpace) canva
return g return g
} }
// rasterGradientCanvas 将公共渐变交给Canvas采样,不改变OFD周期语义
type rasterGradientCanvas struct {
gradient *RasterGradient
space canvas.ColorSpace
}
// At 返回局部坐标的预乘颜色 // At 返回局部坐标的预乘颜色
// 入参: x 横坐标, y 纵坐标 // 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA 渐变颜色 // 返回: color.RGBA 渐变颜色
@@ -236,3 +222,17 @@ func (g rasterGradientCanvas) SetColorSpace(space canvas.ColorSpace) canvas.Grad
} }
return rasterGradientCanvas{gradient: &copy} return rasterGradientCanvas{gradient: &copy}
} }
// init 注册Canvas适配器和默认组合
func init() {
if err := RegisterRenderBackend("canvas", defaultRenderBackends); err != nil {
panic(err)
}
}
// defaultRenderBackends 创建默认组合,调用方可按职责替换任意后端
// 返回: RenderBackends 默认后端组合
func defaultRenderBackends() RenderBackends {
backend := CanvasBackend{}
return RenderBackends{FontResources: SFNTBackend{}, Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
}
+10 -10
View File
@@ -45,16 +45,6 @@ func (r *clipRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canva
} }
} }
// add 合并裁剪轮廓
// 入参: path 裁剪路径
func (r *clipRenderer) add(path *canvas.Path) {
if r.path == nil {
r.path = path
} else {
r.path = unionClipPath(r.path, path)
}
}
// RenderText 合并文字字形轮廓 // RenderText 合并文字字形轮廓
// 入参: text 文字, m 变换矩阵 // 入参: text 文字, m 变换矩阵
func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) { func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
@@ -65,6 +55,16 @@ func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
// 入参: img 图像, m 变换矩阵 // 入参: img 图像, m 变换矩阵
func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {} func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
// add 合并裁剪轮廓
// 入参: path 裁剪路径
func (r *clipRenderer) add(path *canvas.Path) {
if r.path == nil {
r.path = path
} else {
r.path = unionClipPath(r.path, path)
}
}
// buildObjectClipPath 构建对象裁剪路径并应用父级变换 // buildObjectClipPath 构建对象裁剪路径并应用父级变换
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM // 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
// 返回: *canvas.Path 路径对象 // 返回: *canvas.Path 路径对象
+65 -65
View File
@@ -22,6 +22,19 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象
type canvasPageCompiler struct {
page *RasterPage
geometry GeometryBackend
err error
}
// rasterPaddedImage 为旋转采样提供透明边距,共享原图而不复制整幅像素
type rasterPaddedImage struct {
source image.Image
rect image.Rectangle
}
// CompilePage 使用Canvas页面解释器和配置的字体、几何后端生成独立绘制数据 // CompilePage 使用Canvas页面解释器和配置的字体、几何后端生成独立绘制数据
// 入参: r 渲染器, page 页面内容 // 入参: r 渲染器, page 页面内容
// 返回: *RasterPage 绘制页面, error 编译错误 // 返回: *RasterPage 绘制页面, error 编译错误
@@ -48,13 +61,6 @@ func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, e
return result, nil return result, nil
} }
// canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象
type canvasPageCompiler struct {
page *RasterPage
geometry GeometryBackend
err error
}
// Size 返回物理页面尺寸 // Size 返回物理页面尺寸
// 返回: float64 宽度, float64 高度 // 返回: float64 宽度, float64 高度
func (c *canvasPageCompiler) Size() (float64, float64) { return c.page.Width, c.page.Height } func (c *canvasPageCompiler) Size() (float64, float64) { return c.page.Width, c.page.Height }
@@ -84,6 +90,58 @@ func (c *canvasPageCompiler) RenderPath(path *canvas.Path, style canvas.Style, m
} }
} }
// RenderText 展开已定位字形,使用与目标DPI一致的字形精度
// 入参: text 文字, m 变换矩阵
func (c *canvasPageCompiler) RenderText(text *canvas.Text, m canvas.Matrix) {
if c.err == nil {
text.RenderTo(c, m, canvas.DPMM(c.page.DPI/25.4))
}
}
// RenderImage 记录按像素左上原点定位的图片,旋转或斜切时补充透明采样边距
// 入参: img 图片, m 变换矩阵
func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) {
if c.err != nil {
return
}
img = imagePixelSource(img)
if (m[0][1] != 0 || m[1][0] != 0) && (m[0][0] != 0 || m[1][1] == 0) {
bounds := img.Bounds()
img = &rasterPaddedImage{source: img, rect: image.Rect(0, 0, bounds.Dx()+8, bounds.Dy()+8)}
m = m.Translate(-4, -4)
}
h := float64(img.Bounds().Dy())
t := RasterMatrix{m[0][0], -m[1][0], -m[0][1], m[1][1], m[0][2] + m[0][1]*h, c.page.Height - m[1][2] - m[1][1]*h}
c.page.Commands = append(c.page.Commands, RasterCommand{Image: img, Transform: t})
}
// Bounds 返回含四像素边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回预乘八位颜色模型,与实体补边图像保持一致
// 返回: color.Model 颜色模型
func (s *rasterPaddedImage) ColorModel() color.Model { return color.RGBAModel }
// At 读取补边图像的预乘像素,边距及图像外透明
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *rasterPaddedImage) At(x, y int) color.Color {
if !image.Pt(x, y).In(s.rect.Inset(4)) {
return color.RGBA{}
}
b := s.source.Bounds()
return color.RGBAModel.Convert(s.source.At(x-4+b.Min.X, y-4+b.Min.Y))
}
// RGBA64At 返回预乘十六位像素供采样器读取
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 像素颜色
func (s *rasterPaddedImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// fill 转换路径和画刷,未知类型返回错误而非丢弃内容 // fill 转换路径和画刷,未知类型返回错误而非丢弃内容
// 入参: path 路径, paint 画刷, rule 填充规则, m 变换矩阵 // 入参: path 路径, paint 画刷, rule 填充规则, m 变换矩阵
func (c *canvasPageCompiler) fill(path *canvas.Path, paint canvas.Paint, rule canvas.FillRule, m canvas.Matrix) { func (c *canvasPageCompiler) fill(path *canvas.Path, paint canvas.Paint, rule canvas.FillRule, m canvas.Matrix) {
@@ -161,61 +219,3 @@ func canvasRasterPath(path *canvas.Path) ([]RasterSegment, error) {
// 入参: p 默认引擎坐标点 // 入参: p 默认引擎坐标点
// 返回: RasterPoint 独立坐标点 // 返回: RasterPoint 独立坐标点
func rasterPoint(p canvas.Point) RasterPoint { return RasterPoint{X: p.X, Y: p.Y} } func rasterPoint(p canvas.Point) RasterPoint { return RasterPoint{X: p.X, Y: p.Y} }
// RenderText 展开已定位字形,使用与目标DPI一致的字形精度
// 入参: text 文字, m 变换矩阵
func (c *canvasPageCompiler) RenderText(text *canvas.Text, m canvas.Matrix) {
if c.err == nil {
text.RenderTo(c, m, canvas.DPMM(c.page.DPI/25.4))
}
}
// RenderImage 记录按像素左上原点定位的图片,旋转或斜切时补充透明采样边距
// 入参: img 图片, m 变换矩阵
func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) {
if c.err != nil {
return
}
img = imagePixelSource(img)
if (m[0][1] != 0 || m[1][0] != 0) && (m[0][0] != 0 || m[1][1] == 0) {
bounds := img.Bounds()
img = &rasterPaddedImage{source: img, rect: image.Rect(0, 0, bounds.Dx()+8, bounds.Dy()+8)}
m = m.Translate(-4, -4)
}
h := float64(img.Bounds().Dy())
t := RasterMatrix{m[0][0], -m[1][0], -m[0][1], m[1][1], m[0][2] + m[0][1]*h, c.page.Height - m[1][2] - m[1][1]*h}
c.page.Commands = append(c.page.Commands, RasterCommand{Image: img, Transform: t})
}
// rasterPaddedImage 为旋转采样提供透明边距,共享原图而不复制整幅像素
type rasterPaddedImage struct {
source image.Image
rect image.Rectangle
}
// Bounds 返回含四像素边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回预乘八位颜色模型,与实体补边图像保持一致
// 返回: color.Model 颜色模型
func (s *rasterPaddedImage) ColorModel() color.Model { return color.RGBAModel }
// At 读取补边图像的预乘像素,边距及图像外透明
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *rasterPaddedImage) At(x, y int) color.Color {
if !image.Pt(x, y).In(s.rect.Inset(4)) {
return color.RGBA{}
}
b := s.source.Bounds()
return color.RGBAModel.Convert(s.source.At(x-4+b.Min.X, y-4+b.Min.Y))
}
// RGBA64At 返回预乘十六位像素供采样器读取
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 像素颜色
func (s *rasterPaddedImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
+7 -7
View File
@@ -21,6 +21,13 @@ import (
"github.com/tdewolff/font" "github.com/tdewolff/font"
) )
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间
type canvasFontMetrics struct {
*font.SFNT
*fontShaper
outlines *sfntOutliner
}
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配 // ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退 // 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
// 返回: ResolvedFont 字体与来源, error 字体不可用错误 // 返回: ResolvedFont 字体与来源, error 字体不可用错误
@@ -41,13 +48,6 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
return metrics, nil return metrics, nil
} }
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间
type canvasFontMetrics struct {
*font.SFNT
*fontShaper
outlines *sfntOutliner
}
// Write 返回字体数据,不修改字体元信息 // Write 返回字体数据,不修改字体元信息
// 返回: []byte 独立SFNT数据 // 返回: []byte 独立SFNT数据
func (f canvasFontMetrics) Write() []byte { func (f canvasFontMetrics) Write() []byte {
+67 -67
View File
@@ -22,14 +22,6 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// canvasNativeStroke 判断描边是否由Canvas提供
// 入参: geometry 几何后端
// 返回: bool 是否采用原生描边
func canvasNativeStroke(geometry GeometryBackend) bool {
_, ok := geometry.(CanvasBackend)
return ok
}
// Curves 在输出边界转换椭圆弧,其他曲线保持原样 // Curves 在输出边界转换椭圆弧,其他曲线保持原样
// 入参: path 页面路径 // 入参: path 页面路径
// 返回: GeometryPath 贝塞尔路径, error 路径错误 // 返回: GeometryPath 贝塞尔路径, error 路径错误
@@ -41,65 +33,6 @@ func (CanvasBackend) Curves(path GeometryPath) (GeometryPath, error) {
return *geometryFromCanvasPath(p.ReplaceArcs()), nil return *geometryFromCanvasPath(p.ReplaceArcs()), nil
} }
// canvasStrokeOptions 转换Canvas描边样式,虚线由调用方传入
// 入参: width 描边宽度, cap 线帽, join 连接, tolerance 展开误差
// 返回: StrokeOptions 公共描边样式
func canvasStrokeOptions(width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) StrokeOptions {
options := StrokeOptions{Width: width, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, Tolerance: tolerance}
switch cap.(type) {
case canvas.RoundCapper:
options.Cap = "Round"
case canvas.SquareCapper:
options.Cap = "Square"
}
switch value := join.(type) {
case canvas.RoundJoiner:
options.Join = "Round"
case canvas.BevelJoiner:
options.Join = "Bevel"
case canvas.MiterJoiner:
options.MiterLimit = value.Limit
}
return options
}
// canvasStrokePath 通过配置的几何后端生成描边,不修改源路径
// 入参: geometry 几何后端, path Canvas路径, options 描边样式
// 返回: *canvas.Path 描边轮廓, error 几何错误
func canvasStrokePath(geometry GeometryBackend, path *canvas.Path, options StrokeOptions) (*canvas.Path, error) {
outline, err := geometry.Stroke(*geometryFromCanvasPath(path), options)
if err != nil {
return nil, err
}
return geometryToCanvasPath(&outline)
}
// strokeCanvasPath 将文字和复杂画刷的描边交给配置的几何后端
// 入参: path Canvas路径, width 描边宽度, cap 线帽, join 连接
// 返回: *canvas.Path 描边轮廓
func (r *Renderer) strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner) *canvas.Path {
return r.strokeDashedCanvasPath(path, width, cap, join, 0, nil)
}
// strokeDashedCanvasPath 通过公共几何后端展开虚线和线帽,保留零长度绘制段
// 入参: path 路径, width 线宽, cap 线帽, join 连接, offset 虚线偏移, dashes 虚线数组
// 返回: *canvas.Path 描边填充轮廓
func (r *Renderer) strokeDashedCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner, offset float64, dashes []float64) *canvas.Path {
geometry, err := r.Geometry()
if err != nil {
r.renderError = err
return &canvas.Path{}
}
options := canvasStrokeOptions(width, cap, join, canvas.Tolerance)
options.DashOffset, options.Dashes = offset, dashes
result, err := canvasStrokePath(geometry, path, options)
if err != nil {
r.renderError = err
return &canvas.Path{}
}
return result
}
// Path 解析对象的标准紧缩路径并转换到页面坐标 // Path 解析对象的标准紧缩路径并转换到页面坐标
// 入参: object 路径对象 // 入参: object 路径对象
// 返回: GeometryPath 页面路径, error 无效路径错误 // 返回: GeometryPath 页面路径, error 无效路径错误
@@ -245,6 +178,73 @@ func (CanvasBackend) Clip(r *Renderer, clips *Clips, matrix Matrix, parent *Geom
return geometryFromCanvasPath(intersectClipPath(p, clip)), nil return geometryFromCanvasPath(intersectClipPath(p, clip)), nil
} }
// canvasNativeStroke 判断描边是否由Canvas提供
// 入参: geometry 几何后端
// 返回: bool 是否采用原生描边
func canvasNativeStroke(geometry GeometryBackend) bool {
_, ok := geometry.(CanvasBackend)
return ok
}
// canvasStrokeOptions 转换Canvas描边样式,虚线由调用方传入
// 入参: width 描边宽度, cap 线帽, join 连接, tolerance 展开误差
// 返回: StrokeOptions 公共描边样式
func canvasStrokeOptions(width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) StrokeOptions {
options := StrokeOptions{Width: width, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, Tolerance: tolerance}
switch cap.(type) {
case canvas.RoundCapper:
options.Cap = "Round"
case canvas.SquareCapper:
options.Cap = "Square"
}
switch value := join.(type) {
case canvas.RoundJoiner:
options.Join = "Round"
case canvas.BevelJoiner:
options.Join = "Bevel"
case canvas.MiterJoiner:
options.MiterLimit = value.Limit
}
return options
}
// canvasStrokePath 通过配置的几何后端生成描边,不修改源路径
// 入参: geometry 几何后端, path Canvas路径, options 描边样式
// 返回: *canvas.Path 描边轮廓, error 几何错误
func canvasStrokePath(geometry GeometryBackend, path *canvas.Path, options StrokeOptions) (*canvas.Path, error) {
outline, err := geometry.Stroke(*geometryFromCanvasPath(path), options)
if err != nil {
return nil, err
}
return geometryToCanvasPath(&outline)
}
// strokeCanvasPath 将文字和复杂画刷的描边交给配置的几何后端
// 入参: path Canvas路径, width 描边宽度, cap 线帽, join 连接
// 返回: *canvas.Path 描边轮廓
func (r *Renderer) strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner) *canvas.Path {
return r.strokeDashedCanvasPath(path, width, cap, join, 0, nil)
}
// strokeDashedCanvasPath 通过公共几何后端展开虚线和线帽,保留零长度绘制段
// 入参: path 路径, width 线宽, cap 线帽, join 连接, offset 虚线偏移, dashes 虚线数组
// 返回: *canvas.Path 描边填充轮廓
func (r *Renderer) strokeDashedCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner, offset float64, dashes []float64) *canvas.Path {
geometry, err := r.Geometry()
if err != nil {
r.renderError = err
return &canvas.Path{}
}
options := canvasStrokeOptions(width, cap, join, canvas.Tolerance)
options.DashOffset, options.Dashes = offset, dashes
result, err := canvasStrokePath(geometry, path, options)
if err != nil {
r.renderError = err
return &canvas.Path{}
}
return result
}
// geometryToCanvasPath 将页面坐标路径转换为默认几何引擎的向上纵轴 // geometryToCanvasPath 将页面坐标路径转换为默认几何引擎的向上纵轴
// 入参: path 独立几何路径,nil表示不裁剪 // 入参: path 独立几何路径,nil表示不裁剪
// 返回: *canvas.Path 默认引擎路径, error 不支持的路径指令 // 返回: *canvas.Path 默认引擎路径, error 不支持的路径指令
+72 -72
View File
@@ -21,6 +21,13 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓
type boundsRenderer struct {
box Box
collect bool
contours []ObjectContour
}
// MeasureObject 使用默认解释器度量对象,不分配页面像素 // MeasureObject 使用默认解释器度量对象,不分配页面像素
// 入参: r 渲染器, object 图形对象, options 度量上下文 // 入参: r 渲染器, object 图形对象, options 度量上下文
// 返回: ObjectMeasurement 度量结果, error 错误信息 // 返回: ObjectMeasurement 度量结果, error 错误信息
@@ -85,84 +92,12 @@ func (CanvasBackend) MeasureObject(r *Renderer, object GraphicObject, options Me
return ObjectMeasurement{Bounds: bounds.box, Contours: bounds.contours}, renderer.renderError return ObjectMeasurement{Bounds: bounds.box, Contours: bounds.contours}, renderer.renderError
} }
// measureImage 复用图片变换与裁剪语义度量范围,不读取像素数据
// 入参: ctx 度量画布, obj 图片, pageH 页面高度, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) measureImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible || obj.Alpha != nil && *obj.Alpha == 0 {
return
}
box, _ := ParseBox(obj.Boundary)
ctm := NewMatrix(obj.CTM)
if obj.CTM == "" {
ctm = Matrix{a: box.W, d: box.H}
}
m := TranslationMatrix(box.X, box.Y).Multiply(ctm)
if parentCTM != nil {
if boundaryInCTM {
m = parentCTM.Multiply(m)
} else {
m = TranslationMatrix(box.X, box.Y).Multiply(*parentCTM).Multiply(ctm)
}
}
p := canvas.Rectangle(1, 1).Transform(canvas.Matrix{{m.a, m.c, m.e}, {-m.b, -m.d, pageH - m.f}})
clip := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, box.X, box.Y, ctm, parentCTM, boundaryInCTM))
ctx.Renderer.(*boundsRenderer).add(applyClipPath(p, clip))
if obj.Border != nil {
border := *obj.Border
border.BorderColor = (*StrokeColor)(boundsColor((*FillColor)(border.BorderColor)))
obj.Border = &border
r.renderImageBorder(ctx, obj, box, pageH, parentCTM, boundaryInCTM, clip)
}
}
// boundsColor 以底纹的基础颜色和透明度度量轮廓,不修改原画刷
// 入参: color 填充或描边颜色
// 返回: *FillColor 用于度量的颜色
func boundsColor(color *FillColor) *FillColor {
if color == nil || color.Pattern == nil {
return color
}
return &FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace, Alpha: color.Alpha}
}
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓
type boundsRenderer struct {
box Box
collect bool
contours []ObjectContour
}
// Size 返回不限定边界的度量画布尺寸 // Size 返回不限定边界的度量画布尺寸
// 返回: float64 宽度, float64 高度 // 返回: float64 宽度, float64 高度
func (r *boundsRenderer) Size() (float64, float64) { func (r *boundsRenderer) Size() (float64, float64) {
return 0, 0 return 0, 0
} }
// add 合并路径的精确曲线范围并转换为向下的纵轴
// 入参: path 绘制路径
func (r *boundsRenderer) add(path *canvas.Path) {
r.addContour(path, false)
}
// addContour 合并范围并按需保留独立填充区域
// 入参: path 绘制路径, evenOdd 是否使用奇偶规则
func (r *boundsRenderer) addContour(path *canvas.Path, evenOdd bool) {
if path.Empty() {
return
}
rect := path.Bounds()
r.box = unionTextBox(r.box, Box{X: rect.X0, Y: -rect.Y1, W: rect.W(), H: rect.H()})
if r.collect {
outline := &canvas.Path{}
for _, subpath := range path.Split() {
subpath = subpath.Copy()
subpath.Close()
outline = outline.Append(subpath)
}
r.contours = append(r.contours, ObjectContour{Path: outline.Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}).ToSVG(), EvenOdd: evenOdd})
}
}
// RenderPath 收集填充与描边范围,保持线帽、连接和虚线语义 // RenderPath 收集填充与描边范围,保持线帽、连接和虚线语义
// 入参: path 路径, style 绘制样式, m 变换矩阵 // 入参: path 路径, style 绘制样式, m 变换矩阵
func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) { func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) {
@@ -203,6 +138,71 @@ func (CanvasBackend) PageText(r *Renderer, page *PageContent) (*PageText, error)
return renderer.pageText, nil return renderer.pageText, nil
} }
// measureImage 复用图片变换与裁剪语义度量范围,不读取像素数据
// 入参: ctx 度量画布, obj 图片, pageH 页面高度, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) measureImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible || obj.Alpha != nil && *obj.Alpha == 0 {
return
}
box, _ := ParseBox(obj.Boundary)
ctm := NewMatrix(obj.CTM)
if obj.CTM == "" {
ctm = Matrix{a: box.W, d: box.H}
}
m := TranslationMatrix(box.X, box.Y).Multiply(ctm)
if parentCTM != nil {
if boundaryInCTM {
m = parentCTM.Multiply(m)
} else {
m = TranslationMatrix(box.X, box.Y).Multiply(*parentCTM).Multiply(ctm)
}
}
p := canvas.Rectangle(1, 1).Transform(canvas.Matrix{{m.a, m.c, m.e}, {-m.b, -m.d, pageH - m.f}})
clip := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, box.X, box.Y, ctm, parentCTM, boundaryInCTM))
ctx.Renderer.(*boundsRenderer).add(applyClipPath(p, clip))
if obj.Border != nil {
border := *obj.Border
border.BorderColor = (*StrokeColor)(boundsColor((*FillColor)(border.BorderColor)))
obj.Border = &border
r.renderImageBorder(ctx, obj, box, pageH, parentCTM, boundaryInCTM, clip)
}
}
// boundsColor 以底纹的基础颜色和透明度度量轮廓,不修改原画刷
// 入参: color 填充或描边颜色
// 返回: *FillColor 用于度量的颜色
func boundsColor(color *FillColor) *FillColor {
if color == nil || color.Pattern == nil {
return color
}
return &FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace, Alpha: color.Alpha}
}
// add 合并路径的精确曲线范围并转换为向下的纵轴
// 入参: path 绘制路径
func (r *boundsRenderer) add(path *canvas.Path) {
r.addContour(path, false)
}
// addContour 合并范围并按需保留独立填充区域
// 入参: path 绘制路径, evenOdd 是否使用奇偶规则
func (r *boundsRenderer) addContour(path *canvas.Path, evenOdd bool) {
if path.Empty() {
return
}
rect := path.Bounds()
r.box = unionTextBox(r.box, Box{X: rect.X0, Y: -rect.Y1, W: rect.W(), H: rect.H()})
if r.collect {
outline := &canvas.Path{}
for _, subpath := range path.Split() {
subpath = subpath.Copy()
subpath.Close()
outline = outline.Append(subpath)
}
r.contours = append(r.contours, ObjectContour{Path: outline.Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}).ToSVG(), EvenOdd: evenOdd})
}
}
// textPathBox 将字形轮廓转换为页面区域 // textPathBox 将字形轮廓转换为页面区域
// 入参: path 字形轮廓, pageH 页面高度 // 入参: path 字形轮廓, pageH 页面高度
// 返回: Box 字符区域 // 返回: Box 字符区域
+5 -5
View File
@@ -26,6 +26,11 @@ import (
"github.com/tdewolff/canvas/renderers/svg" "github.com/tdewolff/canvas/renderers/svg"
) )
// canvasEPSWriter 在EPS头部写入本库制作软件,不改动绘图内容
type canvasEPSWriter struct {
io.Writer
}
// RenderSVG 使用canvas输出SVG,按模式保留资源和对象分组 // RenderSVG 使用canvas输出SVG,按模式保留资源和对象分组
// 入参: r 渲染器, page 页面内容, writer 输出流, mode 资源模式 // 入参: r 渲染器, page 页面内容, writer 输出流, mode 资源模式
// 返回: SVGResources 外部资源, error 错误信息 // 返回: SVGResources 外部资源, error 错误信息
@@ -74,11 +79,6 @@ func (CanvasBackend) RenderEPS(r *Renderer, page *PageContent, writer io.Writer)
return buffer.Flush() return buffer.Flush()
} }
// canvasEPSWriter 在EPS头部写入本库制作软件,不改动绘图内容
type canvasEPSWriter struct {
io.Writer
}
// Write 写入EPS片段并替换独立的制作软件声明 // Write 写入EPS片段并替换独立的制作软件声明
// 入参: data EPS片段 // 入参: data EPS片段
// 返回: int 已处理字节数, error 写入错误 // 返回: int 已处理字节数, error 写入错误
+15 -15
View File
@@ -27,21 +27,6 @@ type pdfRenderer struct {
glyphPaths map[*canvas.Path]*canvas.Path glyphPaths map[*canvas.Path]*canvas.Path
} }
// glyphPath 复用相似变换下的PDF字形圆弧转换
// 入参: path 缓存字形路径, matrix 字形变换
// 返回: *canvas.Path PDF字形路径
func (r *pdfRenderer) glyphPath(path *canvas.Path, matrix canvas.Matrix) *canvas.Path {
if !matrix.IsSimilarity() {
return path
}
if cached, ok := r.glyphPaths[path]; ok {
return cached
}
converted := path.ReplaceArcs()
r.glyphPaths[path] = converted
return converted
}
// pdfNavigation PDF导航信息 // pdfNavigation PDF导航信息
type pdfNavigation struct { type pdfNavigation struct {
Anchor map[int][]pdfAnchor Anchor map[int][]pdfAnchor
@@ -69,6 +54,21 @@ type pdfOutline struct {
Y float64 Y float64
} }
// glyphPath 复用相似变换下的PDF字形圆弧转换
// 入参: path 缓存字形路径, matrix 字形变换
// 返回: *canvas.Path PDF字形路径
func (r *pdfRenderer) glyphPath(path *canvas.Path, matrix canvas.Matrix) *canvas.Path {
if !matrix.IsSimilarity() {
return path
}
if cached, ok := r.glyphPaths[path]; ok {
return cached
}
converted := path.ReplaceArcs()
r.glyphPaths[path] = converted
return converted
}
// newPDFNavigation 创建PDF导航信息 // newPDFNavigation 创建PDF导航信息
// 入参: renderer 渲染器, doc 文档结构, pages 页面数据 // 入参: renderer 渲染器, doc 文档结构, pages 页面数据
// 返回: *pdfNavigation PDF导航信息, error 几何错误 // 返回: *pdfNavigation PDF导航信息, error 几何错误
+31 -31
View File
@@ -23,6 +23,37 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// shdPaint 渐变画刷
type shdPaint struct {
gradient canvas.Gradient
view canvas.Matrix
extend int
mapType string
period float64
}
// repeatAxialGradient 轴向重复渐变
type repeatAxialGradient struct {
*canvas.LinearGradient
period float64
}
// At 获取重复区间内的渐变颜色
// 入参: x X坐标, y Y坐标
// 返回: color.RGBA 渐变颜色
func (g *repeatAxialGradient) At(x, y float64) color.RGBA {
d := g.End.Sub(g.Start)
t := (canvas.Point{X: x, Y: y}).Sub(g.Start).Dot(d) / d.Dot(d) / g.period
return g.Grad.At(t - math.Floor(t))
}
// SetColorSpace 转换重复渐变颜色空间
// 入参: space 颜色空间
// 返回: canvas.Gradient 渐变对象
func (g *repeatAxialGradient) SetColorSpace(space canvas.ColorSpace) canvas.Gradient {
return &repeatAxialGradient{LinearGradient: g.LinearGradient.SetColorSpace(space).(*canvas.LinearGradient), period: g.period}
}
// drawSampledShading 按输出精度采样后端不能原生表达的局部渐变,不改写OFD对象 // drawSampledShading 按输出精度采样后端不能原生表达的局部渐变,不改写OFD对象
// 入参: ctx 绘制上下文, path 已裁剪轮廓, view 渐变变换, gradient 渐变采样器 // 入参: ctx 绘制上下文, path 已裁剪轮廓, view 渐变变换, gradient 渐变采样器
func (r *Renderer) drawSampledShading(ctx *canvas.Context, path *canvas.Path, view canvas.Matrix, gradient rasterGradientCanvas) { func (r *Renderer) drawSampledShading(ctx *canvas.Context, path *canvas.Path, view canvas.Matrix, gradient rasterGradientCanvas) {
@@ -60,37 +91,6 @@ func (r *Renderer) drawSampledShading(ctx *canvas.Context, path *canvas.Path, vi
ctx.DrawImage(bounds.X0, bounds.Y0, img, canvas.DPMM(scale)) ctx.DrawImage(bounds.X0, bounds.Y0, img, canvas.DPMM(scale))
} }
// shdPaint 渐变画刷
type shdPaint struct {
gradient canvas.Gradient
view canvas.Matrix
extend int
mapType string
period float64
}
// repeatAxialGradient 轴向重复渐变
type repeatAxialGradient struct {
*canvas.LinearGradient
period float64
}
// At 获取重复区间内的渐变颜色
// 入参: x X坐标, y Y坐标
// 返回: color.RGBA 渐变颜色
func (g *repeatAxialGradient) At(x, y float64) color.RGBA {
d := g.End.Sub(g.Start)
t := (canvas.Point{X: x, Y: y}).Sub(g.Start).Dot(d) / d.Dot(d) / g.period
return g.Grad.At(t - math.Floor(t))
}
// SetColorSpace 转换重复渐变颜色空间
// 入参: space 颜色空间
// 返回: canvas.Gradient 渐变对象
func (g *repeatAxialGradient) SetColorSpace(space canvas.ColorSpace) canvas.Gradient {
return &repeatAxialGradient{LinearGradient: g.LinearGradient.SetColorSpace(space).(*canvas.LinearGradient), period: g.period}
}
// newShdPaint 创建渐变画刷 // newShdPaint 创建渐变画刷
// 入参: gradient 渐变对象, extend 延伸方向, mapType 绘制方式, mapUnit 渐变区间长度 // 入参: gradient 渐变对象, extend 延伸方向, mapType 绘制方式, mapUnit 渐变区间长度
// 返回: *shdPaint 渐变画刷 // 返回: *shdPaint 渐变画刷
+64 -64
View File
@@ -55,6 +55,70 @@ type svgObjectGroup struct {
composite bool composite bool
} }
// RenderImage 绘制图片,分离资源时保持原始编码、尺寸和变换
// 入参: img 图片对象, m 变换矩阵
func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
if s.imageNames == nil {
s.SVG.RenderImage(img, m)
return
}
name, ok := s.imageNames[img]
if !ok {
resource := SVGImage{MIME: "image/png"}
if encoded, ok := img.(*canvasimage.Image); ok {
resource.MIME, resource.Data = encoded.Mimetype, encoded.Bytes
} else {
var buffer bytes.Buffer
if err := png.Encode(&buffer, img); err != nil {
s.err = err
return
}
resource.Data = buffer.Bytes()
}
name = fmt.Sprintf("ofdgo-image-%x", sha256.Sum256(resource.Data))
resource.Name = name
s.imageNames[img] = name
if !s.seen[name] {
s.seen[name] = true
s.images = append(s.images, resource)
}
}
_, height := s.Size()
size := img.Bounds().Size()
fmt.Fprintf(s.writer, `<image transform="%s" width="%d" height="%d" xlink:href="%s"/>`, m.Translate(0, float64(size.Y)).ToSVG(height), size.X, size.Y, name)
}
// RenderText 绘制文字并记录实际使用的字体资源
// 入参: text 文字对象, m 变换矩阵
func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
if text.Empty() {
return
}
font := text.MostCommonFontFace().Font
resource, ok := s.renderer.canvasState().svgFontCache[font]
if !ok {
data := fontSFNTData(font.SFNT)
resource = SVGFont{
Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()),
Weight: font.Style().CSS(),
Style: "normal",
Data: data,
}
if font.Style().Italic() {
resource.Style = "italic"
}
s.renderer.canvasState().svgFontCache[font] = resource
}
if !s.seen[resource.Name] {
s.seen[resource.Name] = true
s.fonts = append(s.fonts, resource)
fmt.Fprintf(&s.styles, ".%s{font-family:%s!important}", resource.Name, resource.Name)
}
s.SetClass(resource.Name)
s.SVG.RenderText(text, m)
s.SetClass()
}
// renderSVGResources 渲染SVG并收集外部资源 // renderSVGResources 渲染SVG并收集外部资源
// 入参: page 页面内容, writer 输出流, images 是否分离图片, objects 是否标识页面直接对象 // 入参: page 页面内容, writer 输出流, images 是否分离图片, objects 是否标识页面直接对象
// 返回: SVGResources 外部资源, error 错误信息 // 返回: SVGResources 外部资源, error 错误信息
@@ -149,67 +213,3 @@ func (s *svgResourceRenderer) skipObjects(count int) {
s.objectStack[len(s.objectStack)-1].next += count s.objectStack[len(s.objectStack)-1].next += count
} }
} }
// RenderImage 绘制图片,分离资源时保持原始编码、尺寸和变换
// 入参: img 图片对象, m 变换矩阵
func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
if s.imageNames == nil {
s.SVG.RenderImage(img, m)
return
}
name, ok := s.imageNames[img]
if !ok {
resource := SVGImage{MIME: "image/png"}
if encoded, ok := img.(*canvasimage.Image); ok {
resource.MIME, resource.Data = encoded.Mimetype, encoded.Bytes
} else {
var buffer bytes.Buffer
if err := png.Encode(&buffer, img); err != nil {
s.err = err
return
}
resource.Data = buffer.Bytes()
}
name = fmt.Sprintf("ofdgo-image-%x", sha256.Sum256(resource.Data))
resource.Name = name
s.imageNames[img] = name
if !s.seen[name] {
s.seen[name] = true
s.images = append(s.images, resource)
}
}
_, height := s.Size()
size := img.Bounds().Size()
fmt.Fprintf(s.writer, `<image transform="%s" width="%d" height="%d" xlink:href="%s"/>`, m.Translate(0, float64(size.Y)).ToSVG(height), size.X, size.Y, name)
}
// RenderText 绘制文字并记录实际使用的字体资源
// 入参: text 文字对象, m 变换矩阵
func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
if text.Empty() {
return
}
font := text.MostCommonFontFace().Font
resource, ok := s.renderer.canvasState().svgFontCache[font]
if !ok {
data := fontSFNTData(font.SFNT)
resource = SVGFont{
Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()),
Weight: font.Style().CSS(),
Style: "normal",
Data: data,
}
if font.Style().Italic() {
resource.Style = "italic"
}
s.renderer.canvasState().svgFontCache[font] = resource
}
if !s.seen[resource.Name] {
s.seen[resource.Name] = true
s.fonts = append(s.fonts, resource)
fmt.Fprintf(&s.styles, ".%s{font-family:%s!important}", resource.Name, resource.Name)
}
s.SetClass(resource.Name)
s.SVG.RenderText(text, m)
s.SetClass()
}
+83 -83
View File
@@ -31,33 +31,6 @@ type textPageTokens struct {
parents []string parents []string
} }
// Token 读取文字页面所需的XML节点
// 返回: xml.Token XML节点, error 错误信息
func (d *textPageTokens) Token() (xml.Token, error) {
for {
token, err := d.Decoder.Token()
if err != nil {
return nil, err
}
switch node := token.(type) {
case xml.StartElement:
if len(d.parents) > 0 && (node.Name.Local == "PathObject" || node.Name.Local == "ImageObject") {
switch d.parents[len(d.parents)-1] {
case "Layer", "PageBlock", "CompositeGraphicUnit", "CompositeObject":
if err := d.Decoder.Skip(); err != nil {
return nil, err
}
continue
}
}
d.parents = append(d.parents, node.Name.Local)
case xml.EndElement:
d.parents = d.parents[:len(d.parents)-1]
}
return token, nil
}
}
// PageText 页面文字,按绘制顺序保留文本对象,不包含图像和签名外观 // PageText 页面文字,按绘制顺序保留文本对象,不包含图像和签名外观
type PageText struct { type PageText struct {
Runs []TextRun `json:"runs"` Runs []TextRun `json:"runs"`
@@ -92,6 +65,33 @@ type TextMatch struct {
Boxes []Box `json:"boxes"` Boxes []Box `json:"boxes"`
} }
// Token 读取文字页面所需的XML节点
// 返回: xml.Token XML节点, error 错误信息
func (d *textPageTokens) Token() (xml.Token, error) {
for {
token, err := d.Decoder.Token()
if err != nil {
return nil, err
}
switch node := token.(type) {
case xml.StartElement:
if len(d.parents) > 0 && (node.Name.Local == "PathObject" || node.Name.Local == "ImageObject") {
switch d.parents[len(d.parents)-1] {
case "Layer", "PageBlock", "CompositeGraphicUnit", "CompositeObject":
if err := d.Decoder.Skip(); err != nil {
return nil, err
}
continue
}
}
d.parents = append(d.parents, node.Name.Local)
case xml.EndElement:
d.parents = d.parents[:len(d.parents)-1]
}
return token, nil
}
}
// PageText 提取页面及启用注释的原文,复用当前编译器的字体和几何语义 // PageText 提取页面及启用注释的原文,复用当前编译器的字体和几何语义
// 入参: page 页面内容 // 入参: page 页面内容
// 返回: *PageText 页面文字, error 错误信息 // 返回: *PageText 页面文字, error 错误信息
@@ -135,6 +135,62 @@ func (p *PageText) WriteTo(writer io.Writer) (int64, error) {
return p.writeTo(writer, false) return p.writeTo(writer, false)
} }
// Search 按原文进行忽略大小写的字面匹配,不跨文本对象拼接或识别图像
// 入参: query 搜索文字
// 返回: []TextMatch 匹配结果及上下文
func (p *PageText) Search(query string) []TextMatch {
var matches []TextMatch
_ = p.SearchEach(context.Background(), query, func(match TextMatch) error {
matches = append(matches, match)
return nil
})
return matches
}
// SearchEach 逐项返回忽略大小写的字面匹配,不累积全部结果,在对象与匹配之间检查取消
// 入参: ctx 取消上下文, query 搜索文字, visit 匹配访问函数,可返回错误停止
// 返回: error 取消或访问错误
func (p *PageText) SearchEach(ctx context.Context, query string, visit func(TextMatch) error) error {
if err := ctx.Err(); err != nil {
return err
}
return p.searchEach(ctx, foldText(query), visit)
}
// SearchText 逐页搜索原文并立即返回匹配,不保留整本文字或识别图像
// 入参: ctx 取消上下文,页面提取之间及匹配时检查, query 搜索文字, visit 零基页码和匹配访问函数, indices 页码,省略则全部,按原页序去重
// 返回: error 页面提取、取消或访问错误
func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int, TextMatch) error, indices ...int) error {
if err := ctx.Err(); err != nil {
return err
}
if query == "" {
return nil
}
count, err := r.Reader.PageCount()
if err != nil {
return err
}
indices, err = exportPageIndices(count, indices)
if err != nil {
return err
}
query = foldText(query)
for _, index := range indices {
if err := ctx.Err(); err != nil {
return err
}
text, err := r.PageTextByIndex(index)
if err != nil {
return fmt.Errorf("failed to search page %d: %w", index+1, err)
}
if err := text.searchEach(ctx, query, func(match TextMatch) error { return visit(index, match) }); err != nil {
return err
}
}
return ctx.Err()
}
// writeTo 输出非空文本对象,可在首个对象前分隔上一页 // writeTo 输出非空文本对象,可在首个对象前分隔上一页
// 入参: writer 输出流, separator 是否需要前导换行 // 入参: writer 输出流, separator 是否需要前导换行
// 返回: int64 已写字节数, error 写入错误 // 返回: int64 已写字节数, error 写入错误
@@ -167,28 +223,6 @@ func (p *PageText) writeTo(writer io.Writer, separator bool) (int64, error) {
return written, nil return written, nil
} }
// Search 按原文进行忽略大小写的字面匹配,不跨文本对象拼接或识别图像
// 入参: query 搜索文字
// 返回: []TextMatch 匹配结果及上下文
func (p *PageText) Search(query string) []TextMatch {
var matches []TextMatch
_ = p.SearchEach(context.Background(), query, func(match TextMatch) error {
matches = append(matches, match)
return nil
})
return matches
}
// SearchEach 逐项返回忽略大小写的字面匹配,不累积全部结果,在对象与匹配之间检查取消
// 入参: ctx 取消上下文, query 搜索文字, visit 匹配访问函数,可返回错误停止
// 返回: error 取消或访问错误
func (p *PageText) SearchEach(ctx context.Context, query string, visit func(TextMatch) error) error {
if err := ctx.Err(); err != nil {
return err
}
return p.searchEach(ctx, foldText(query), visit)
}
// searchEach 使用预折叠的查询执行搜索,供跨页搜索复用 // searchEach 使用预折叠的查询执行搜索,供跨页搜索复用
// 入参: ctx 取消上下文, query 已折叠查询, visit 匹配访问函数 // 入参: ctx 取消上下文, query 已折叠查询, visit 匹配访问函数
// 返回: error 取消或访问错误 // 返回: error 取消或访问错误
@@ -240,40 +274,6 @@ func (p *PageText) searchEach(ctx context.Context, query string, visit func(Text
return ctx.Err() return ctx.Err()
} }
// SearchText 逐页搜索原文并立即返回匹配,不保留整本文字或识别图像
// 入参: ctx 取消上下文,页面提取之间及匹配时检查, query 搜索文字, visit 零基页码和匹配访问函数, indices 页码,省略则全部,按原页序去重
// 返回: error 页面提取、取消或访问错误
func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int, TextMatch) error, indices ...int) error {
if err := ctx.Err(); err != nil {
return err
}
if query == "" {
return nil
}
count, err := r.Reader.PageCount()
if err != nil {
return err
}
indices, err = exportPageIndices(count, indices)
if err != nil {
return err
}
query = foldText(query)
for _, index := range indices {
if err := ctx.Err(); err != nil {
return err
}
text, err := r.PageTextByIndex(index)
if err != nil {
return fmt.Errorf("failed to search page %d: %w", index+1, err)
}
if err := text.searchEach(ctx, query, func(match TextMatch) error { return visit(index, match) }); err != nil {
return err
}
}
return ctx.Err()
}
// foldText 统一Unicode大小写等价字符,保留字符数量 // foldText 统一Unicode大小写等价字符,保留字符数量
// 入参: text 原文 // 入参: text 原文
// 返回: string 匹配用文字 // 返回: string 匹配用文字
+30 -30
View File
@@ -21,16 +21,6 @@ import (
"strings" "strings"
) )
// Outline 使用默认几何后端获取SVG轮廓,自定义后端使用Renderer.PathOutline
// 返回: string SVG路径数据, error 错误信息
func (p PathObject) Outline() (string, error) {
r := &Renderer{backends: defaultRenderBackends()}
return r.PathOutline(p)
}
// ShapeKind 基本图形类型
type ShapeKind string
const ( const (
ShapeLine ShapeKind = "line" ShapeLine ShapeKind = "line"
ShapeArrow ShapeKind = "arrow" ShapeArrow ShapeKind = "arrow"
@@ -39,6 +29,16 @@ const (
ShapeEllipse ShapeKind = "ellipse" ShapeEllipse ShapeKind = "ellipse"
) )
// ShapeKind 基本图形类型
type ShapeKind string
// Outline 使用默认几何后端获取SVG轮廓,自定义后端使用Renderer.PathOutline
// 返回: string SVG路径数据, error 错误信息
func (p PathObject) Outline() (string, error) {
r := &Renderer{backends: defaultRenderBackends()}
return r.PathOutline(p)
}
// NewShape 创建使用标准紧缩路径的图形,默认黑色描边、不填充 // NewShape 创建使用标准紧缩路径的图形,默认黑色描边、不填充
// 可调整返回对象的颜色、线宽等属性后通过Editor.AddObject写入 // 可调整返回对象的颜色、线宽等属性后通过Editor.AddObject写入
// 水平、垂直直线使用正尺寸边界,路径端点不变 // 水平、垂直直线使用正尺寸边界,路径端点不变
@@ -222,26 +222,6 @@ func (p PathObject) ReshapeFrame(box Box) (PathObject, error) {
return p, nil return p, nil
} }
// sameShapePath 比较基本路径的命令与数值,忽略数值格式差异
// 入参: a、b 路径词元
// 返回: bool 是否相同
func sameShapePath(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, token := range a {
if token == b[i] {
continue
}
x, errX := strconv.ParseFloat(token, 64)
y, errY := strconv.ParseFloat(b[i], 64)
if errX != nil || errY != nil || !finite(x) || !finite(y) || math.Abs(x-y) > 1e-10*math.Max(1, math.Max(math.Abs(x), math.Abs(y))) {
return false
}
}
return true
}
// Reshape 调整基本路径在所在坐标系中的几何范围,保留对象标识、方向、缩放和绘制属性,不缩放线宽 // Reshape 调整基本路径在所在坐标系中的几何范围,保留对象标识、方向、缩放和绘制属性,不缩放线宽
// 返回对象可通过Editor.UpdateObject写入;仅支持Shape可识别的路径 // 返回对象可通过Editor.UpdateObject写入;仅支持Shape可识别的路径
// 入参: box 新几何范围,直线使用起点和有符号的端点位移 // 入参: box 新几何范围,直线使用起点和有符号的端点位移
@@ -262,6 +242,26 @@ func (p PathObject) ReshapeLine(kind ShapeKind, box Box) (PathObject, error) {
return p.reshape(kind, box) return p.reshape(kind, box)
} }
// sameShapePath 比较基本路径的命令与数值,忽略数值格式差异
// 入参: a、b 路径词元
// 返回: bool 是否相同
func sameShapePath(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, token := range a {
if token == b[i] {
continue
}
x, errX := strconv.ParseFloat(token, 64)
y, errY := strconv.ParseFloat(b[i], 64)
if errX != nil || errY != nil || !finite(x) || !finite(y) || math.Abs(x-y) > 1e-10*math.Max(1, math.Max(math.Abs(x), math.Abs(y))) {
return false
}
}
return true
}
// lineShapeKind 判断基本图形是否使用直线端点 // lineShapeKind 判断基本图形是否使用直线端点
// 入参: kind 图形类型 // 入参: kind 图形类型
// 返回: bool 是否为直线或箭头 // 返回: bool 是否为直线或箭头
+11 -11
View File
@@ -23,9 +23,6 @@ import (
"strings" "strings"
) )
// SignType 签名类型
type SignType string
const ( const (
// SignTypeSeal 印章签名 // SignTypeSeal 印章签名
SignTypeSeal SignType = "Seal" SignTypeSeal SignType = "Seal"
@@ -33,6 +30,9 @@ const (
SignTypeSign SignType = "Sign" SignTypeSign SignType = "Sign"
) )
// SignType 签名类型
type SignType string
// Signatures 签名列表 // Signatures 签名列表
type Signatures struct { type Signatures struct {
XMLName xml.Name `xml:"Signatures"` XMLName xml.Name `xml:"Signatures"`
@@ -107,6 +107,14 @@ type SignatureReference struct {
CheckValue string `xml:"CheckValue"` CheckValue string `xml:"CheckValue"`
} }
// Stamp 印章信息结构
type Stamp struct {
Box Box
Clip *Box
Type string
Data []byte
}
// SignatureStampPositions 获取签名外观位置信息 // SignatureStampPositions 获取签名外观位置信息
// 入参: stamps 签名外观列表 // 入参: stamps 签名外观列表
// 返回: []SignatureStampPosition 签名外观位置信息, error 错误信息 // 返回: []SignatureStampPosition 签名外观位置信息, error 错误信息
@@ -421,14 +429,6 @@ func probeImageMedia(data []byte) (string, []byte) {
return normalizeSealType(format), data return normalizeSealType(format), data
} }
// Stamp 印章信息结构
type Stamp struct {
Box Box
Clip *Box
Type string
Data []byte
}
// addStamp 添加印章到页面 // addStamp 添加印章到页面
// 入参: pageID 页面ID, box 印章区域, clip 裁剪区域, sType 印章类型, data 印章数据 // 入参: pageID 页面ID, box 印章区域, clip 裁剪区域, sType 印章类型, data 印章数据
func (r *Reader) addStamp(pageID string, box Box, clip *Box, sType string, data []byte) { func (r *Reader) addStamp(pageID string, box Box, clip *Box, sType string, data []byte) {
+21 -21
View File
@@ -46,10 +46,31 @@ type SignaturePolicy struct {
// signaturePolicyRejection 区分策略拒绝与解析失败 // signaturePolicyRejection 区分策略拒绝与解析失败
type signaturePolicyRejection struct{ message string } type signaturePolicyRejection struct{ message string }
// signatureCoverageNode 收集OFD结构中的包内路径引用
type signatureCoverageNode struct {
XMLName xml.Name
Attrs []xml.Attr `xml:",any,attr"`
Text string `xml:",chardata"`
Children []signatureCoverageNode `xml:",any"`
}
// Error 返回策略拒绝原因 // Error 返回策略拒绝原因
// 返回: string 错误信息 // 返回: string 错误信息
func (e signaturePolicyRejection) Error() string { return e.message } func (e signaturePolicyRejection) Error() string { return e.message }
// WithSignaturePolicy 设置签名策略
// 入参: policy 签名策略
// 返回: SignatureVerifyOption 验签选项
func WithSignaturePolicy(policy SignaturePolicy) SignatureVerifyOption {
policy.OFDVersions = append([]string(nil), policy.OFDVersions...)
policy.SESVersions = append([]int(nil), policy.SESVersions...)
policy.SignedDataVersions = append([]int(nil), policy.SignedDataVersions...)
policy.DigestMethods = append([]string(nil), policy.DigestMethods...)
policy.SignatureMethods = append([]string(nil), policy.SignatureMethods...)
policy.RequiredFiles = append([]string(nil), policy.RequiredFiles...)
return func(o *signatureVerifyOptions) { o.Policy = &policy }
}
// rejectSignaturePolicy 构造可识别的策略拒绝错误 // rejectSignaturePolicy 构造可识别的策略拒绝错误
// 入参: format 消息模板, values 模板参数 // 入参: format 消息模板, values 模板参数
// 返回: error 策略错误 // 返回: error 策略错误
@@ -66,19 +87,6 @@ func (report *SignatureVerifyReport) applySignaturePolicyError(err error) {
} }
} }
// WithSignaturePolicy 设置签名策略
// 入参: policy 签名策略
// 返回: SignatureVerifyOption 验签选项
func WithSignaturePolicy(policy SignaturePolicy) SignatureVerifyOption {
policy.OFDVersions = append([]string(nil), policy.OFDVersions...)
policy.SESVersions = append([]int(nil), policy.SESVersions...)
policy.SignedDataVersions = append([]int(nil), policy.SignedDataVersions...)
policy.DigestMethods = append([]string(nil), policy.DigestMethods...)
policy.SignatureMethods = append([]string(nil), policy.SignatureMethods...)
policy.RequiredFiles = append([]string(nil), policy.RequiredFiles...)
return func(o *signatureVerifyOptions) { o.Policy = &policy }
}
// applySignaturePolicy 应用版本和算法限制 // applySignaturePolicy 应用版本和算法限制
// 入参: o 验签选项, ofdVersion OFD版本, sesVersion SES版本, method 签名算法, digest 摘要算法 // 入参: o 验签选项, ofdVersion OFD版本, sesVersion SES版本, method 签名算法, digest 摘要算法
func (report *SignatureVerifyReport) applySignaturePolicy(o *signatureVerifyOptions, ofdVersion string, sesVersion int, method, digest string) { func (report *SignatureVerifyReport) applySignaturePolicy(o *signatureVerifyOptions, ofdVersion string, sesVersion int, method, digest string) {
@@ -284,14 +292,6 @@ func (r *Reader) signatureCoveragePath(name string) string {
return name return name
} }
// signatureCoverageNode 收集OFD结构中的包内路径引用
type signatureCoverageNode struct {
XMLName xml.Name
Attrs []xml.Attr `xml:",any,attr"`
Text string `xml:",chardata"`
Children []signatureCoverageNode `xml:",any"`
}
// signatureCoverageReferences 收集标准OFD引用的传递闭包且包含跨文档资源 // signatureCoverageReferences 收集标准OFD引用的传递闭包且包含跨文档资源
// 入参: root 文档入口, required 待补充的必需文件集合 // 入参: root 文档入口, required 待补充的必需文件集合
// 返回: error 引用文件无法读取或解析时的错误 // 返回: error 引用文件无法读取或解析时的错误
+31 -31
View File
@@ -45,37 +45,6 @@ type ParsedSignatureSeal struct {
Height float64 Height float64
} }
// ParseSignatureSeal 解析独立SES印章供展示使用,不验证授权、有效期或制章者信任
// 入参: data DER编码的电子印章
// 返回: *ParsedSignatureSeal 印章信息及独立图像副本, error 解析错误
func ParseSignatureSeal(data []byte) (*ParsedSignatureSeal, error) {
var raw asn1.RawValue
rest, err := asn1.Unmarshal(data, &raw)
if err != nil || len(rest) != 0 || raw.Tag != asn1.TagSequence || raw.Class != asn1.ClassUniversal {
return nil, fmt.Errorf("invalid SES seal DER")
}
seal, err := parseSESSeal(raw)
if err != nil {
return nil, err
}
items, _ := asn1Children(raw.Bytes)
info, _ := asn1Children(items[0].Bytes)
picture, _ := asn1Children(info[3].Bytes)
width, err := asn1Integer(picture[2])
if err != nil || width <= 0 {
return nil, fmt.Errorf("invalid seal picture width")
}
height, err := asn1Integer(picture[3])
if err != nil || height <= 0 {
return nil, fmt.Errorf("invalid seal picture height")
}
format, image := probeSealMedia(seal.PicData)
if len(image) == 0 || format != seal.PicType {
return nil, fmt.Errorf("invalid seal picture")
}
return &ParsedSignatureSeal{Info: seal.Info, Type: format, Data: append([]byte(nil), image...), Width: float64(width), Height: float64(height)}, nil
}
// sesSignature SES签章值 // sesSignature SES签章值
type sesSignature struct { type sesSignature struct {
Timestamp []byte Timestamp []byte
@@ -137,6 +106,37 @@ type sesVerifyResult struct {
SignatureTime time.Time SignatureTime time.Time
} }
// ParseSignatureSeal 解析独立SES印章供展示使用,不验证授权、有效期或制章者信任
// 入参: data DER编码的电子印章
// 返回: *ParsedSignatureSeal 印章信息及独立图像副本, error 解析错误
func ParseSignatureSeal(data []byte) (*ParsedSignatureSeal, error) {
var raw asn1.RawValue
rest, err := asn1.Unmarshal(data, &raw)
if err != nil || len(rest) != 0 || raw.Tag != asn1.TagSequence || raw.Class != asn1.ClassUniversal {
return nil, fmt.Errorf("invalid SES seal DER")
}
seal, err := parseSESSeal(raw)
if err != nil {
return nil, err
}
items, _ := asn1Children(raw.Bytes)
info, _ := asn1Children(items[0].Bytes)
picture, _ := asn1Children(info[3].Bytes)
width, err := asn1Integer(picture[2])
if err != nil || width <= 0 {
return nil, fmt.Errorf("invalid seal picture width")
}
height, err := asn1Integer(picture[3])
if err != nil || height <= 0 {
return nil, fmt.Errorf("invalid seal picture height")
}
format, image := probeSealMedia(seal.PicData)
if len(image) == 0 || format != seal.PicType {
return nil, fmt.Errorf("invalid seal picture")
}
return &ParsedSignatureSeal{Info: seal.Info, Type: format, Data: append([]byte(nil), image...), Width: float64(width), Height: float64(height)}, nil
}
// parseSESSignature 解析SES签章值 // parseSESSignature 解析SES签章值
// 入参: data 签章值数据 // 入参: data 签章值数据
// 返回: *sesSignature SES签章值, error 错误信息 // 返回: *sesSignature SES签章值, error 错误信息
+11 -11
View File
@@ -99,17 +99,6 @@ func WithSignatureTimestamp(options SignatureTimestampOptions) SignatureVerifyOp
return func(o *signatureVerifyOptions) { o.Timestamp = &options } return func(o *signatureVerifyOptions) { o.Timestamp = &options }
} }
// cloneSignatureEvidence 深拷贝离线证据
// 入参: values 原证据列表
// 返回: [][]byte 独立证据列表
func cloneSignatureEvidence(values [][]byte) [][]byte {
out := make([][]byte, len(values))
for i := range values {
out[i] = bytes.Clone(values[i])
}
return out
}
// VerifySignatureTimestamp 验证RFC3161令牌与原始字节的绑定 // VerifySignatureTimestamp 验证RFC3161令牌与原始字节的绑定
// 入参: token DER编码TimeStampToken, data 被时间戳保护的原始字节, options 验证选项 // 入参: token DER编码TimeStampToken, data 被时间戳保护的原始字节, options 验证选项
// 返回: SignatureTimestampReport 验证报告 // 返回: SignatureTimestampReport 验证报告
@@ -122,6 +111,17 @@ func VerifySignatureTimestamp(token, data []byte, options SignatureTimestampOpti
return report return report
} }
// cloneSignatureEvidence 深拷贝离线证据
// 入参: values 原证据列表
// 返回: [][]byte 独立证据列表
func cloneSignatureEvidence(values [][]byte) [][]byte {
out := make([][]byte, len(values))
for i := range values {
out[i] = bytes.Clone(values[i])
}
return out
}
// verifySignatureTimestamp 检查令牌绑定、签名与TSA信任 // verifySignatureTimestamp 检查令牌绑定、签名与TSA信任
// 入参: token 令牌DER, data 原始字节, o 验证选项, report 结果 // 入参: token 令牌DER, data 原始字节, o 验证选项, report 结果
// 返回: error 验证错误 // 返回: error 验证错误
+42 -42
View File
@@ -39,6 +39,8 @@ import (
"github.com/emmansun/gmsm/smx509" "github.com/emmansun/gmsm/smx509"
) )
var signatureMethodReplacer = strings.NewReplacer("-", "", "_", "", " ", "")
// SignatureVerifyReport 签名验证报告 // SignatureVerifyReport 签名验证报告
// Valid表示签名完整性、签名时间语义及调用方指定的全部策略均通过 // Valid表示签名完整性、签名时间语义及调用方指定的全部策略均通过
// Checked表示对应检查已得出结论, 未检查时不能将OK的零值视为失败 // Checked表示对应检查已得出结论, 未检查时不能将OK的零值视为失败
@@ -103,36 +105,6 @@ type SignatureVerifyReport struct {
Revocations []SignatureRevocationReport Revocations []SignatureRevocationReport
} }
// IntegrityValid 判断签名完整性是否有效
// 返回: bool 是否有效
func (report SignatureVerifyReport) IntegrityValid() bool {
sealOK := report.Type == SignTypeSign || report.SealChecked && report.SealOK
return report.Error == "" && report.DigestOK && referencesOK(report.References) &&
report.DataHashChecked && report.DataHashOK && report.SignedValueChecked && report.SignedValueOK &&
sealOK && report.SealMatchOK && report.CertChecked && report.CertOK
}
// TrustedValid 判断签名是否可信有效
// 返回: bool 签名完整性、时间语义、证书信任及证书有效期是否均验证通过
func (report SignatureVerifyReport) TrustedValid() bool {
return report.IntegrityValid() && report.certificatePolicyOK() && report.CertTrustChecked && report.CertTimeChecked
}
// HasFailure 判断已完成的签名检查是否存在失败
// 返回: bool 是否存在失败, 未完成检查时仍需判断Valid和Error
func (report SignatureVerifyReport) HasFailure() bool {
for _, ref := range report.References {
if ref.Checked && !ref.OK {
return true
}
}
return report.DataHashChecked && !report.DataHashOK ||
report.SignedValueChecked && !report.SignedValueOK ||
report.SealChecked && !report.SealOK ||
report.SealMatchChecked && !report.SealMatchOK ||
report.CertChecked && !report.CertOK || !report.certificatePolicyOK()
}
// SignatureCertInfo 签名证书信息 // SignatureCertInfo 签名证书信息
type SignatureCertInfo struct { type SignatureCertInfo struct {
Raw []byte Raw []byte
@@ -180,11 +152,39 @@ type signatureVerifyOptions struct {
DocRoot string DocRoot string
} }
var signatureMethodReplacer = strings.NewReplacer("-", "", "_", "", " ", "")
// SignatureVerifyOption 签名验证选项函数 // SignatureVerifyOption 签名验证选项函数
type SignatureVerifyOption func(*signatureVerifyOptions) type SignatureVerifyOption func(*signatureVerifyOptions)
// IntegrityValid 判断签名完整性是否有效
// 返回: bool 是否有效
func (report SignatureVerifyReport) IntegrityValid() bool {
sealOK := report.Type == SignTypeSign || report.SealChecked && report.SealOK
return report.Error == "" && report.DigestOK && referencesOK(report.References) &&
report.DataHashChecked && report.DataHashOK && report.SignedValueChecked && report.SignedValueOK &&
sealOK && report.SealMatchOK && report.CertChecked && report.CertOK
}
// TrustedValid 判断签名是否可信有效
// 返回: bool 签名完整性、时间语义、证书信任及证书有效期是否均验证通过
func (report SignatureVerifyReport) TrustedValid() bool {
return report.IntegrityValid() && report.certificatePolicyOK() && report.CertTrustChecked && report.CertTimeChecked
}
// HasFailure 判断已完成的签名检查是否存在失败
// 返回: bool 是否存在失败, 未完成检查时仍需判断Valid和Error
func (report SignatureVerifyReport) HasFailure() bool {
for _, ref := range report.References {
if ref.Checked && !ref.OK {
return true
}
}
return report.DataHashChecked && !report.DataHashOK ||
report.SignedValueChecked && !report.SignedValueOK ||
report.SealChecked && !report.SealOK ||
report.SealMatchChecked && !report.SealMatchOK ||
report.CertChecked && !report.CertOK || !report.certificatePolicyOK()
}
// WithSignatureCert 添加数字签名验证证书 // WithSignatureCert 添加数字签名验证证书
// 入参: cert DER或PEM编码证书 // 入参: cert DER或PEM编码证书
// 返回: SignatureVerifyOption 签名验证选项 // 返回: SignatureVerifyOption 签名验证选项
@@ -232,16 +232,6 @@ func WithSignatureVerifyTime(t time.Time) SignatureVerifyOption {
} }
} }
// appendSignatureCerts 追加签名证书
// 入参: dst 目标证书列表, certs DER或PEM编码证书列表
// 返回: [][]byte 证书列表
func appendSignatureCerts(dst [][]byte, certs ...[]byte) [][]byte {
for _, cert := range certs {
dst = append(dst, parseSignatureCerts(cert)...)
}
return dst
}
// VerifySignaturesBytes 验证OFD字节数据签名 // VerifySignaturesBytes 验证OFD字节数据签名
// 入参: data OFD字节数据, opts 签名验证选项 // 入参: data OFD字节数据, opts 签名验证选项
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息 // 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
@@ -342,6 +332,16 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti
return reports, nil return reports, nil
} }
// appendSignatureCerts 追加签名证书
// 入参: dst 目标证书列表, certs DER或PEM编码证书列表
// 返回: [][]byte 证书列表
func appendSignatureCerts(dst [][]byte, certs ...[]byte) [][]byte {
for _, cert := range certs {
dst = append(dst, parseSignatureCerts(cert)...)
}
return dst
}
// verifySignature 验证单个签名 // verifySignature 验证单个签名
// 入参: sigListPath 签名列表路径, sigRef 签名引用, options 验证选项 // 入参: sigListPath 签名列表路径, sigRef 签名引用, options 验证选项
// 返回: SignatureVerifyReport 签名验证报告 // 返回: SignatureVerifyReport 签名验证报告
+13 -13
View File
@@ -29,9 +29,6 @@ import (
"time" "time"
) )
// SignatureWriteMode 签署方式
type SignatureWriteMode uint8
const ( const (
// SignatureAppend 追加签署,要求原活动签名有效且未保护签名列表 // SignatureAppend 追加签署,要求原活动签名有效且未保护签名列表
SignatureAppend SignatureWriteMode = iota SignatureAppend SignatureWriteMode = iota
@@ -39,6 +36,9 @@ const (
SignatureReplace SignatureReplace
) )
// SignatureWriteMode 签署方式
type SignatureWriteMode uint8
// SignatureWriteOptions 最终包签署选项,不修改Editor或调用方的原件 // SignatureWriteOptions 最终包签署选项,不修改Editor或调用方的原件
// SM2数字签名使用GB/T 35275-2017消息结构;RSA/ECDSA使用裸签名,验签需WithSignatureCert // SM2数字签名使用GB/T 35275-2017消息结构;RSA/ECDSA使用裸签名,验签需WithSignatureCert
// Seal仅接受已有合法SES v4电子印章,使用SM2/SM3,不声明GM/T 0031-2025兼容 // Seal仅接受已有合法SES v4电子印章,使用SM2/SM3,不声明GM/T 0031-2025兼容
@@ -64,6 +64,16 @@ type SignatureWriteOptions struct {
progress editorProgress progress editorProgress
} }
// signatureWriteInfo 写出签名信息,省略数字签名中不存在的印章
type signatureWriteInfo struct {
Provider SignatureProvider `xml:"Provider"`
SignatureMethod string `xml:"SignatureMethod"`
SignatureDateTime string `xml:"SignatureDateTime"`
References SignatureReferences `xml:"References"`
Stamps []SignatureStamp `xml:"StampAnnot,omitempty"`
Seal *SignatureSeal `xml:"Seal,omitempty"`
}
// SignPackage 签署最终OFD包,失败时返回nil且不修改输入 // SignPackage 签署最终OFD包,失败时返回nil且不修改输入
// 入参: data 最终包字节, options 签署选项 // 入参: data 最终包字节, options 签署选项
// 返回: []byte 签署后的完整包, error 错误信息 // 返回: []byte 签署后的完整包, error 错误信息
@@ -436,16 +446,6 @@ func signatureWriteIDs(list *Signatures, listPath string, parts map[string][]byt
return id, stamps, nil return id, stamps, nil
} }
// signatureWriteInfo 写出签名信息,省略数字签名中不存在的印章
type signatureWriteInfo struct {
Provider SignatureProvider `xml:"Provider"`
SignatureMethod string `xml:"SignatureMethod"`
SignatureDateTime string `xml:"SignatureDateTime"`
References SignatureReferences `xml:"References"`
Stamps []SignatureStamp `xml:"StampAnnot,omitempty"`
Seal *SignatureSeal `xml:"Seal,omitempty"`
}
// signatureWriteXML 编码带OFD命名空间的签名文件 // signatureWriteXML 编码带OFD命名空间的签名文件
// 入参: name 根节点名称, value 内容 // 入参: name 根节点名称, value 内容
// 返回: []byte XML, error 错误信息 // 返回: []byte XML, error 错误信息
+7 -7
View File
@@ -37,6 +37,13 @@ import (
"github.com/emmansun/gmsm/smx509" "github.com/emmansun/gmsm/smx509"
) )
// signatureWriteKey 已验证签署身份
type signatureWriteKey struct {
cert *smx509.Certificate
method string
digest string
}
// ParseSignatureIdentity 解析签署私钥和证书,并检查公私钥对应关系 // ParseSignatureIdentity 解析签署私钥和证书,并检查公私钥对应关系
// 支持普通PKCS8、SEC1、PKCS1以及加密PKCS8,不支持传统加密PEM // 支持普通PKCS8、SEC1、PKCS1以及加密PKCS8,不支持传统加密PEM
// 不加载系统信任或暗中信任该证书,返回的SM2/RSA私钥同时实现crypto.Decrypter // 不加载系统信任或暗中信任该证书,返回的SM2/RSA私钥同时实现crypto.Decrypter
@@ -97,13 +104,6 @@ func ParseSignatureIdentity(certificate, privateKey []byte, password ...[]byte)
return signer, bytes.Clone(cert.Raw), nil return signer, bytes.Clone(cert.Raw), nil
} }
// signatureWriteKey 已验证签署身份
type signatureWriteKey struct {
cert *smx509.Certificate
method string
digest string
}
// signatureWriteIdentity 检查签署身份、证书信任与印章授权 // signatureWriteIdentity 检查签署身份、证书信任与印章授权
// 制章证书按制章时间验证,不以签署时间代替制章时间 // 制章证书按制章时间验证,不以签署时间代替制章时间
// 入参: options 签署选项 // 入参: options 签署选项
+24 -24
View File
@@ -21,6 +21,18 @@ import (
"unicode/utf16" "unicode/utf16"
) )
// textGlyph 绘制字形
type textGlyph struct {
Text string
GlyphID int
}
// textGlyphTransform 字符到字形变换
type textGlyphTransform struct {
CodeCount int
Glyphs []textGlyph
}
// Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示 // Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示
// 返回: string 原文 // 返回: string 原文
func (obj TextObject) Text() string { func (obj TextObject) Text() string {
@@ -38,6 +50,18 @@ func (obj TextObject) Text() string {
return text.String() return text.String()
} }
// GetDeltaX 获取X轴偏移量数组
// 返回: []float64 偏移量数组
func (tc *TextCode) GetDeltaX() []float64 {
return parseFloats(tc.DeltaX)
}
// GetDeltaY 获取Y轴偏移量数组
// 返回: []float64 偏移量数组
func (tc *TextCode) GetDeltaY() []float64 {
return parseFloats(tc.DeltaY)
}
// textCodeLineBreak 判断横向文字的行首回退,或有足够行距且水平范围重叠的对齐段落 // textCodeLineBreak 判断横向文字的行首回退,或有足够行距且水平范围重叠的对齐段落
// 入参: index 文本编码索引 // 入参: index 文本编码索引
// 返回: bool 是否换行 // 返回: bool 是否换行
@@ -67,18 +91,6 @@ func (obj TextObject) textCodeLineBreak(index int) bool {
return x <= right return x <= right
} }
// textGlyph 绘制字形
type textGlyph struct {
Text string
GlyphID int
}
// textGlyphTransform 字符到字形变换
type textGlyphTransform struct {
CodeCount int
Glyphs []textGlyph
}
// textObjectFontID 获取文本对象字体ID // textObjectFontID 获取文本对象字体ID
// 入参: text 文本对象 // 入参: text 文本对象
// 返回: string 字体ID // 返回: string 字体ID
@@ -305,15 +317,3 @@ func textCodeRunes(value string) []rune {
} }
return runes[:written] return runes[:written]
} }
// GetDeltaX 获取X轴偏移量数组
// 返回: []float64 偏移量数组
func (tc *TextCode) GetDeltaX() []float64 {
return parseFloats(tc.DeltaX)
}
// GetDeltaY 获取Y轴偏移量数组
// 返回: []float64 偏移量数组
func (tc *TextCode) GetDeltaY() []float64 {
return parseFloats(tc.DeltaY)
}
+7 -7
View File
@@ -29,6 +29,13 @@ type configuredTextShaper struct {
options TextShapeOptions options TextShapeOptions
} }
// ShapeText 使用固定选项塑形单行原文
// 入参: value 原文, size 毫米字号
// 返回: []ShapedGlyph 字形, error 塑形错误
func (s configuredTextShaper) ShapeText(value string, size float64) ([]ShapedGlyph, error) {
return s.ShapeTextWithOptions(value, size, s.options)
}
// layoutShapedText 将显式塑形结果保存为标准文字定位,成功前不修改对象 // layoutShapedText 将显式塑形结果保存为标准文字定位,成功前不修改对象
// 入参: obj 文字对象, value 原文, options 排版选项, metrics 字体度量, width 行宽, lineHeight 行高, hScale 横向比例, baseline 首行基线 // 入参: obj 文字对象, value 原文, options 排版选项, metrics 字体度量, width 行宽, lineHeight 行高, hScale 横向比例, baseline 首行基线
// 返回: error 塑形或排版错误 // 返回: error 塑形或排版错误
@@ -198,13 +205,6 @@ func bidiClusterShifts(glyphs []ShapedGlyph, extra []float64) map[int]float64 {
return shifts return shifts
} }
// ShapeText 使用固定选项塑形单行原文
// 入参: value 原文, size 毫米字号
// 返回: []ShapedGlyph 字形, error 塑形错误
func (s configuredTextShaper) ShapeText(value string, size float64) ([]ShapedGlyph, error) {
return s.ShapeTextWithOptions(value, size, s.options)
}
// shapeTextLine 检查可选后端的簇映射,按原文字符分配塑形步进 // shapeTextLine 检查可选后端的簇映射,按原文字符分配塑形步进
// 入参: shaper 字体塑形器, runes 单行字符, font 字体标识, size 字号, scale 横向比例, spacing 字距 // 入参: shaper 字体塑形器, runes 单行字符, font 字体标识, size 字号, scale 横向比例, spacing 字距
// 返回: []ShapedGlyph 字形, []float64 含字距的字符步进, error 塑形错误 // 返回: []ShapedGlyph 字形, []float64 含字距的字符步进, error 塑形错误
+76 -76
View File
@@ -35,16 +35,21 @@ const ofdNamespace = "http://www.ofdspec.org/2016"
// editorProgress 保存准备阶段的进度检查点,内存预览使用nil // editorProgress 保存准备阶段的进度检查点,内存预览使用nil
type editorProgress func(stage string, completed, total int) error type editorProgress func(stage string, completed, total int) error
// report 回报进度并原样传递调用方停止原因 // ofdCountingWriter 记录实际输出字节数
// 入参: stage 阶段, completed 已处理项, total 总项数 type ofdCountingWriter struct {
// 返回: error 调用方停止原因 writer io.Writer
func (progress editorProgress) report(stage string, completed, total int) error { count int64
if progress != nil {
return progress(stage, completed, total)
}
return nil
} }
// ofdXML 使用标准ofd命名空间写出XML
type ofdXML struct {
encoder *xml.Encoder
err error
}
// ofdAttrs OFD节点属性
type ofdAttrs []xml.Attr
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出 // WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集 // 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样 // 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
@@ -59,47 +64,6 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
return e.writePlaintext(writer) return e.writePlaintext(writer)
} }
// writePlaintext 写出未加密包,供显式明文输出与加密封装共用
// 入参: writer 输出流
// 返回: int64 写入字节数, error 错误信息
func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
if err := e.validate(); err != nil {
return 0, err
}
progress := editorProgress(e.OnWriteProgress)
fonts, err := e.subsetFonts(progress)
if err != nil {
return 0, err
}
if e.source != nil {
return e.writeSource(writer, fonts, progress)
}
if err := progress.report("write", 0, 0); err != nil {
return 0, err
}
output := &ofdCountingWriter{writer: writer}
archive := zip.NewWriter(output)
err = e.writeParts(func(name string, data []byte, compressed bool) error {
if subset, ok := fonts[name]; ok {
data = subset
}
method := uint16(zip.Deflate)
if compressed {
method = zip.Store
}
entry, err := archive.CreateHeader(&zip.FileHeader{Name: name, Method: method})
if err != nil {
return err
}
_, err = entry.Write(data)
return err
}, progress)
if err == nil {
err = archive.Close()
}
return output.count, err
}
// WritePagesTo 按指定顺序将页面另存为独立OFD,不修改当前文档或撤销记录 // WritePagesTo 按指定顺序将页面另存为独立OFD,不修改当前文档或撤销记录
// 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转 // 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转
// 快照只准备所选页面,关联引用由页面导入流程统一筛选 // 快照只准备所选页面,关联引用由页面导入流程统一筛选
@@ -160,6 +124,69 @@ func (e *Editor) Reader() (*Reader, error) {
return e.reader(nil) return e.reader(nil)
} }
// Write 写入并累计字节数
// 入参: data 数据
// 返回: int 字节数, error 错误信息
func (w *ofdCountingWriter) Write(data []byte) (int, error) {
n, err := w.writer.Write(data)
w.count += int64(n)
if err == nil && n != len(data) {
err = io.ErrShortWrite
}
return n, err
}
// report 回报进度并原样传递调用方停止原因
// 入参: stage 阶段, completed 已处理项, total 总项数
// 返回: error 调用方停止原因
func (progress editorProgress) report(stage string, completed, total int) error {
if progress != nil {
return progress(stage, completed, total)
}
return nil
}
// writePlaintext 写出未加密包,供显式明文输出与加密封装共用
// 入参: writer 输出流
// 返回: int64 写入字节数, error 错误信息
func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
if err := e.validate(); err != nil {
return 0, err
}
progress := editorProgress(e.OnWriteProgress)
fonts, err := e.subsetFonts(progress)
if err != nil {
return 0, err
}
if e.source != nil {
return e.writeSource(writer, fonts, progress)
}
if err := progress.report("write", 0, 0); err != nil {
return 0, err
}
output := &ofdCountingWriter{writer: writer}
archive := zip.NewWriter(output)
err = e.writeParts(func(name string, data []byte, compressed bool) error {
if subset, ok := fonts[name]; ok {
data = subset
}
method := uint16(zip.Deflate)
if compressed {
method = zip.Store
}
entry, err := archive.CreateHeader(&zip.FileHeader{Name: name, Method: method})
if err != nil {
return err
}
_, err = entry.Write(data)
return err
}, progress)
if err == nil {
err = archive.Close()
}
return output.count, err
}
// reader 生成内存快照,保存时复用准备进度,预览时不触发回调 // reader 生成内存快照,保存时复用准备进度,预览时不触发回调
// 入参: progress 准备进度回调 // 入参: progress 准备进度回调
// 返回: *Reader 阅读器, error 错误信息 // 返回: *Reader 阅读器, error 错误信息
@@ -525,30 +552,6 @@ func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace)
x.end("Res") x.end("Res")
} }
// ofdCountingWriter 记录实际输出字节数
type ofdCountingWriter struct {
writer io.Writer
count int64
}
// Write 写入并累计字节数
// 入参: data 数据
// 返回: int 字节数, error 错误信息
func (w *ofdCountingWriter) Write(data []byte) (int, error) {
n, err := w.writer.Write(data)
w.count += int64(n)
if err == nil && n != len(data) {
err = io.ErrShortWrite
}
return n, err
}
// ofdXML 使用标准ofd命名空间写出XML
type ofdXML struct {
encoder *xml.Encoder
err error
}
// newOFDXML 创建XML编码器 // newOFDXML 创建XML编码器
// 入参: writer 输出流 // 入参: writer 输出流
// 返回: *ofdXML 编码器 // 返回: *ofdXML 编码器
@@ -601,9 +604,6 @@ func (x *ofdXML) finish() error {
return x.encoder.Close() return x.encoder.Close()
} }
// ofdAttrs OFD节点属性
type ofdAttrs []xml.Attr
// add 添加非空属性 // add 添加非空属性
// 入参: name 属性名, value 属性值 // 入参: name 属性名, value 属性值
func (a *ofdAttrs) add(name, value string) { func (a *ofdAttrs) add(name, value string) {