mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
5e0da3da28
commit
ee6c9f6a16
82 files changed
+4314
-4308
No files matched your search
@@ -9,7 +9,7 @@ require (
|
||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||
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/image v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
|
||||
@@ -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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||
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-20260928063322-fedbd99f692e/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260928071431-2842b88597d4 h1:R0jk+8LHdihgZdzvLHJQgX1ptkPfiK45rSvb7i9L3Fw=
|
||||
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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
|
||||
+35
-35
@@ -42,6 +42,21 @@ import (
|
||||
"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 分块传递导出数据
|
||||
type exportWriter struct {
|
||||
write js.Value
|
||||
@@ -115,21 +130,6 @@ type annotationOptions struct {
|
||||
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 将数据块交给浏览器保存
|
||||
// 入参: data 导出数据
|
||||
// 返回: int 写入长度, error 错误信息
|
||||
@@ -184,26 +184,6 @@ func (r *browserReaderAt) ReadAt(data []byte, offset int64) (int, error) {
|
||||
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接口并阻塞运行
|
||||
func RunWASM() {
|
||||
registerCallback("ofdgoOpen", openDocument)
|
||||
@@ -291,6 +271,26 @@ func RunWASM() {
|
||||
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 注册浏览器回调函数
|
||||
// 入参: name 回调名称, fn 回调函数
|
||||
func registerCallback(name string, fn func([]js.Value) (any, error)) {
|
||||
|
||||
+162
-162
@@ -266,38 +266,6 @@ func Open(data []byte, opts OpenOptions) (*Session, error) {
|
||||
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 关闭文档会话
|
||||
// 返回: error 错误信息
|
||||
func (s *Session) Close() error {
|
||||
@@ -333,59 +301,6 @@ func (s *Session) SetFonts(fonts []FontFile) error {
|
||||
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 获取指定页面文字,复用当前会话的文字索引
|
||||
// 入参: index 页面索引
|
||||
// 返回: *ofdgo.PageText 页面文字, error 错误信息
|
||||
@@ -417,16 +332,6 @@ func (s *Session) PageTextString(index int) (string, error) {
|
||||
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 搜索指定页面,复用当前会话的文字索引
|
||||
// 入参: index 页面索引, query 搜索文字
|
||||
// 返回: []ofdgo.TextMatch 匹配结果, error 错误信息
|
||||
@@ -479,23 +384,6 @@ func (s *Session) Summary() DocumentInfo {
|
||||
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 获取无需逐页扫描的附件、签名及字体匹配结果
|
||||
// 返回: DocumentDetails 文档补充信息
|
||||
func (s *Session) Details() DocumentDetails {
|
||||
@@ -629,62 +517,12 @@ func (s *Session) SVGFontData(name string) ([]byte, error) {
|
||||
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 获取导出格式
|
||||
// 返回: []ExportFormat 导出格式列表
|
||||
func ExportFormats() []ExportFormat {
|
||||
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 导出单页
|
||||
// 入参: index 页面索引, value 导出格式, dpi 图片DPI, writer 输出流
|
||||
// 返回: ExportFormat 导出格式, error 错误信息
|
||||
@@ -739,6 +577,168 @@ func (s *Session) ExportPDF(writer io.Writer, indices ...int) error {
|
||||
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 加密状态
|
||||
func (s *Session) encryptionInfo() EncryptionInfo {
|
||||
|
||||
+15
-15
@@ -126,21 +126,6 @@ type actionSource struct {
|
||||
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 解析跳转目标
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: 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 获取动作浮点属性
|
||||
// 入参: start 起始节点, name 属性名
|
||||
// 返回: float64 属性值
|
||||
|
||||
+46
-46
@@ -22,6 +22,13 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
SVGEmbedded SVGMode = iota
|
||||
SVGExternalFonts
|
||||
SVGExternalResources
|
||||
SVGObjects
|
||||
)
|
||||
|
||||
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
||||
var ErrBackendUnavailable = errors.New("render backend unavailable")
|
||||
|
||||
@@ -68,13 +75,6 @@ type ObjectMeasurement struct {
|
||||
// SVGMode 指定SVG资源封装方式,Objects模式还需保留稳定的对象分组
|
||||
type SVGMode uint8
|
||||
|
||||
const (
|
||||
SVGEmbedded SVGMode = iota
|
||||
SVGExternalFonts
|
||||
SVGExternalResources
|
||||
SVGObjects
|
||||
)
|
||||
|
||||
// SVGBackend 输出SVG及资源,不得修改源页面或省略不支持的内容
|
||||
// renderer提供文档和排版能力,输出时不得再次调用RenderToSVG系列方法
|
||||
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)}
|
||||
}
|
||||
|
||||
// backendName 获取已配置的后端标识
|
||||
// 入参: backend 后端实例
|
||||
// 返回: string 后端标识,未配置时为空
|
||||
func backendName(backend Backend) string {
|
||||
if backend == nil {
|
||||
return ""
|
||||
}
|
||||
return backend.Name()
|
||||
}
|
||||
|
||||
// WithRenderBackends 替换完整后端组合,未配置的能力不会沿用默认实现
|
||||
// 入参: backends 后端组合
|
||||
// 返回: 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 判断不可变值配置是否未变,包含指针的配置重新应用时刷新可变状态
|
||||
// 入参: left 原后端, right 新后端
|
||||
// 返回: bool 是否为同一配置
|
||||
@@ -202,32 +231,3 @@ func immutableBackendValue(value reflect.Value) bool {
|
||||
}
|
||||
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
@@ -57,27 +57,6 @@ type readerOptions struct {
|
||||
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编码
|
||||
type EncryptionRecipient struct {
|
||||
UserName string
|
||||
@@ -110,6 +89,27 @@ type encryptionState struct {
|
||||
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 获取文档加密状态,不暴露口令或私钥
|
||||
// 返回: EncryptionInfo 加密状态
|
||||
func (r *Reader) Encryption() EncryptionInfo {
|
||||
|
||||
+17
-17
@@ -88,6 +88,23 @@ type encryptionEntry struct {
|
||||
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根节点前解密
|
||||
// 入参: options 阅读选项
|
||||
// 返回: error 错误信息
|
||||
@@ -406,23 +423,6 @@ func inheritedEncryptionOptions(seed encryptionSeed, credential Credentials, pro
|
||||
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 加密所有原条目,再生成标准入口、密钥描述与映射
|
||||
// 入参: parts 明文条目, options 加密策略, progress 取消与进度检查
|
||||
// 返回: []byte 加密包, error 错误信息
|
||||
|
||||
+29
-29
@@ -190,6 +190,12 @@ type ValidPeriod struct {
|
||||
EndDate string `xml:"EndDate,attr"`
|
||||
}
|
||||
|
||||
// print 打印权限节点
|
||||
type print struct {
|
||||
Printable *bool `xml:"Printable,attr"`
|
||||
Copies *int `xml:"Copies,attr"`
|
||||
}
|
||||
|
||||
// OutlineInfos 获取目录层级和目标页码
|
||||
// 返回: []OutlineInfo 目录节点信息
|
||||
func (doc *Document) OutlineInfos() []OutlineInfo {
|
||||
@@ -207,31 +213,6 @@ func (doc *Document) OutlineInfos() []OutlineInfo {
|
||||
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 解析文档并应用权限默认值
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
@@ -327,10 +308,29 @@ func (p *Permissions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// print 打印权限节点
|
||||
type print struct {
|
||||
Printable *bool `xml:"Printable,attr"`
|
||||
Copies *int `xml:"Copies,attr"`
|
||||
// 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
|
||||
}
|
||||
|
||||
// defaultPermissions 获取默认文档权限
|
||||
|
||||
+191
-191
@@ -66,6 +66,28 @@ type Editor struct {
|
||||
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 设置编辑与几何度量使用的外部字体目录,不嵌入或替换文档字体
|
||||
// 入参: dirs 字体目录,空参数清除目录配置
|
||||
func (e *Editor) SetFontDirs(dirs ...string) {
|
||||
@@ -117,44 +139,6 @@ func (e *Editor) Backends() RenderBackends {
|
||||
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 创建空白文档,保存前至少添加一页
|
||||
// 返回: *Editor 文档编辑器
|
||||
func NewEditor() *Editor {
|
||||
@@ -226,21 +210,6 @@ func (e *Editor) Page(index int) (*PageContent, error) {
|
||||
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 添加页面及默认正文图层
|
||||
// 入参: width 页面宽度, height 页面高度
|
||||
// 返回: int 页面索引, error 错误信息
|
||||
@@ -363,23 +332,6 @@ func (e *Editor) DeletePages(indexes []int) error {
|
||||
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 移动页面,保留页面及对象标识
|
||||
// 入参: from 原页面索引, to 移动后的页面索引
|
||||
// 返回: error 错误信息
|
||||
@@ -445,15 +397,6 @@ func (e *Editor) MovePages(indexes []int, to int) error {
|
||||
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 调整页面尺寸,不缩放或移动页面内的对象
|
||||
// 入参: index 页面索引, width 页面宽度, height 页面高度
|
||||
// 返回: error 错误信息
|
||||
@@ -503,13 +446,6 @@ func (e *Editor) ResizePages(indexes []int, width, height float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextID 分配文档内唯一标识
|
||||
// 返回: string 对象标识
|
||||
func (e *Editor) nextID() string {
|
||||
e.maxID++
|
||||
return strconv.Itoa(e.maxID)
|
||||
}
|
||||
|
||||
// AddImage 注册PNG、JPEG或JBIG2图片,重复资源复用标识,引用后写入文档
|
||||
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
|
||||
// 入参: data 图片数据
|
||||
@@ -552,21 +488,6 @@ func (e *Editor) AddImage(data []byte) (string, error) {
|
||||
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
|
||||
// 支持基本颜色、直接资源引用及路径裁剪,不支持动作、渐变及复合图元
|
||||
// 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据
|
||||
@@ -658,6 +579,175 @@ func (e *Editor) MoveObject(page int, id string, to int) error {
|
||||
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 查找页面各图层中的对象
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: *Layer 所属图层, int 对象索引, error 错误信息
|
||||
@@ -835,49 +925,6 @@ func cloneEditorObject(object GraphicObject) (GraphicObject, error) {
|
||||
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 变换独立对象副本,保留绘制属性和编辑中的段落信息
|
||||
// 入参: object 对象副本, dx 横向位移, dy 纵向位移, scale 正缩放比例
|
||||
// 返回: GraphicObject 变换后的对象, error 错误信息
|
||||
@@ -940,36 +987,6 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
|
||||
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重复编码
|
||||
// 入参: value 定位数值, scale 缩放比例
|
||||
// 返回: string 缩放后的数值
|
||||
@@ -985,23 +1002,6 @@ func scaleTextNumbers(value string, scale float64) string {
|
||||
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 校验并补全标准文字定位,避免依赖阅读器的缺省字距补偿
|
||||
// 入参: obj 文字对象
|
||||
// 返回: error 错误信息
|
||||
|
||||
+192
-192
@@ -28,14 +28,18 @@ type editorObjectPosition struct {
|
||||
index int
|
||||
}
|
||||
|
||||
// compareEditorPosition 比较对象在页面结构中的先后顺序
|
||||
// 入参: a、b 对象位置
|
||||
// 返回: int 比较结果
|
||||
func compareEditorPosition(a, b editorObjectPosition) int {
|
||||
if value := cmp.Compare(a.layer, b.layer); value != 0 {
|
||||
return value
|
||||
// ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象
|
||||
type ObjectPosition struct {
|
||||
Layer string
|
||||
Container string
|
||||
Index int
|
||||
Count int
|
||||
}
|
||||
return cmp.Compare(a.index, b.index)
|
||||
|
||||
// ObjectInfo 对象的编辑能力及直接容器位置
|
||||
type ObjectInfo struct {
|
||||
Capabilities ObjectCapabilities
|
||||
Position ObjectPosition
|
||||
}
|
||||
|
||||
// Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息
|
||||
@@ -134,20 +138,6 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
|
||||
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 批量获取页面对象能力与位置,不重复扫描同一容器
|
||||
// 每次按当前页面计算,变更和撤销后无需失效旧缓存
|
||||
// 入参: page 页面索引
|
||||
@@ -211,6 +201,187 @@ func (e *Editor) ObjectPosition(page int, id string) (ObjectPosition, error) {
|
||||
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 获取同一直接容器的对象索引,不跨越图层或页块
|
||||
// 入参: page 页面索引, layer 图层, id 对象标识
|
||||
// 返回: *editorXML 原始容器, []int 对象索引
|
||||
@@ -253,45 +424,6 @@ func (e *Editor) objectOrderParent(nodes map[string]*editorXML, id string) *edit
|
||||
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 调整选区层级,保持选中及未选中对象各自的相对顺序
|
||||
// 入参: objects 同容器对象, selected 选中状态, order 排序方向
|
||||
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 计算等间距平移,位置相同时按原绘制顺序排序
|
||||
// 入参: boxes 可见范围, indexes 绘制位置, axis 分布方向
|
||||
// 返回: []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 校验内容更新或几何与外观变换,原子提交跨图层选区
|
||||
// 入参: page 页面索引, objects 新对象, preserveContent 是否保留原内容
|
||||
// 返回: error 错误信息
|
||||
@@ -545,66 +632,6 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve
|
||||
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 计算对象范围到目标边缘或中心的平移
|
||||
// 入参: box 对象范围, target 目标范围, alignment 对齐方式
|
||||
// 返回: Matrix 页面平移
|
||||
@@ -633,33 +660,6 @@ func editorAlignment(box, target Box, alignment string) 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 校验同页选择,返回内部只读对象与对应索引
|
||||
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||
// 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息
|
||||
|
||||
+137
-137
@@ -85,67 +85,6 @@ type editorCompositeCrop struct {
|
||||
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 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级
|
||||
// 入参: page 页面索引, path 复合对象路径
|
||||
// 返回: []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 将旋转或镜像中心移至选区中心
|
||||
// 入参: box 选区, matrix 方向矩阵
|
||||
// 返回: Matrix 页面变换
|
||||
@@ -616,82 +692,6 @@ func (e *Editor) compositeMemberStyle(node *editorCompositeNode) (GraphicObject,
|
||||
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 按成员实际轮廓计算独立页面变换并提交一次历史记录
|
||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, arrange 排列计算
|
||||
// 返回: error 错误信息
|
||||
|
||||
+140
-140
@@ -53,6 +53,146 @@ func (e *Editor) PasteCompositeSelection(page int, selection *CompositeSelection
|
||||
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, ©)
|
||||
}
|
||||
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] = ©
|
||||
}
|
||||
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 固定内部快照的页面几何和外观,粘贴失败时回收新增绘制资源
|
||||
// 入参: selection 独立内部快照, paste 目标范围粘贴操作
|
||||
// 返回: error 错误信息
|
||||
@@ -224,146 +364,6 @@ func (n *editorCompositeNode) convertCoordinates(parent Matrix, inward bool) err
|
||||
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, ©)
|
||||
}
|
||||
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] = ©
|
||||
}
|
||||
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展开并校验的范围
|
||||
// 入参: path 逐层成员序号
|
||||
// 返回: *editorCompositeNode 范围节点
|
||||
|
||||
+109
-109
@@ -22,28 +22,6 @@ import (
|
||||
"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 将内部图片与页面矩形取交集,保留原裁剪、蒙版、变换和边框
|
||||
// 只收窄可见区域,撤销恢复上次裁剪,不重采样图片
|
||||
// 入参: 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 仅移除本次编辑追加的裁剪,保留输入文件中的裁剪、蒙版和其他属性
|
||||
// 入参: page 页面索引, path 父路径, index 图片序号
|
||||
// 返回: 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})
|
||||
}
|
||||
|
||||
// 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 按原像素比例适应或填充内部图片框,保留原始裁剪和蒙版
|
||||
// 入参: page 页面索引, path 父路径, index 图片序号, mode 为contain或cover
|
||||
// 返回: 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)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -21,18 +21,6 @@ import (
|
||||
"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 删除内部成员,保留容器、未选对象及其他实例,一次撤销恢复全部
|
||||
// 入参: page 页面索引, path 父复合路径, indexes 成员序号
|
||||
// 返回: error 错误信息
|
||||
@@ -85,33 +73,6 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int,
|
||||
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, ©, 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 调整同一直接容器内的绘制顺序,不改变页块和其他实例
|
||||
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, order 为up、down、top或bottom
|
||||
// 返回: []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 获取内部基本路径的页面几何,矩形和椭圆要求轴对齐,其他成员返回空类型
|
||||
// 返回: 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, ©, 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 获取路径所在坐标到页面的变换,兼容旧式内联边界
|
||||
// 返回: 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}
|
||||
}
|
||||
|
||||
// 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 按页面几何修改单个内部路径,可选修改箭头类型
|
||||
// 入参: page 页面索引, path 父路径, index 成员序号, kind 目标类型,空值保持原类型, box 页面几何, lineOnly 是否仅接受直线
|
||||
// 返回: error 错误信息
|
||||
|
||||
+23
-23
@@ -50,6 +50,29 @@ func (e *Editor) EraseObjectsPath(page int, ids []string, points []Point) error
|
||||
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 校验闭合折线并按奇偶规则构建擦除区域
|
||||
// 入参: points 页面毫米坐标
|
||||
// 返回: GeometryPath 擦除区域, error 错误信息
|
||||
@@ -76,29 +99,6 @@ func (e *Editor) editorEraseRegion(points []Point) (GeometryPath, error) {
|
||||
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 按页面区域裁剪内部成员,全部成功后提交一次历史
|
||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, region 擦除区域
|
||||
// 返回: error 错误信息
|
||||
|
||||
@@ -16,9 +16,6 @@ package ofdgo
|
||||
|
||||
import "errors"
|
||||
|
||||
// EditReason 编辑操作受限的原因,不依赖错误描述的语言或文案
|
||||
type EditReason string
|
||||
|
||||
const (
|
||||
// EditUnsupportedObject 对象包含尚不支持编辑的特性
|
||||
EditUnsupportedObject EditReason = "unsupportedObject"
|
||||
@@ -38,6 +35,9 @@ const (
|
||||
EditInvalidObject EditReason = "invalidObject"
|
||||
)
|
||||
|
||||
// EditReason 编辑操作受限的原因,不依赖错误描述的语言或文案
|
||||
type EditReason string
|
||||
|
||||
// EditError 编辑受限原因及原始错误,可通过errors.As取得Code和更具体的诊断
|
||||
type EditError struct {
|
||||
Code EditReason
|
||||
|
||||
+47
-47
@@ -19,6 +19,14 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
// editorChange 页面或对象操作及其修订标识
|
||||
type editorChange struct {
|
||||
undo func(*Editor)
|
||||
redo func(*Editor)
|
||||
before uint64
|
||||
after uint64
|
||||
}
|
||||
|
||||
// Transaction 将多项修改作为一次原子操作提交,失败时保留原文档及历史
|
||||
// 回调仅操作传入的编辑器,不可保留该实例供后续使用
|
||||
// 入参: edit 批量修改回调
|
||||
@@ -49,53 +57,6 @@ func (e *Editor) Transaction(edit func(*Editor) error) error {
|
||||
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 设置撤销和重做记录的总上限,默认关闭,非正数关闭并释放记录
|
||||
// 缩减上限时优先保留最近的撤销记录,不影响当前文档及修订标识
|
||||
// 记录页面、对象及SetInfo操作,不包含Info的直接修改及字体、图片注册,资源继续共享
|
||||
@@ -160,6 +121,45 @@ func (e *Editor) Revision() uint64 {
|
||||
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 分配修订标识并为已完成的有效修改预留记录
|
||||
// 返回: *editorChange 记录,未启用历史时为nil
|
||||
func (e *Editor) recordChange() *editorChange {
|
||||
|
||||
+20
-20
@@ -78,6 +78,14 @@ type ObjectImportOptions struct {
|
||||
OnProgress func(stage string, completed, total int) error
|
||||
}
|
||||
|
||||
// editorProgressReader 为输入流提供可取消的分块读取检查点
|
||||
type editorProgressReader struct {
|
||||
io.Reader
|
||||
progress editorProgress
|
||||
stage string
|
||||
completed int
|
||||
}
|
||||
|
||||
// ImportObjects 迁移选区及引用资源,不改变来源与目标页面结构,提交一次撤销记录
|
||||
// 保留对象原文与图层继承,同页链接映射到目标页,无法迁移的跨页链接明确报错
|
||||
// 返回后可关闭来源阅读器,后续修改来源不影响副本
|
||||
@@ -238,6 +246,18 @@ func (e *Editor) ImportPagesWithOptions(source *Reader, indexes []int, at int, o
|
||||
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 迁移页面,同文档复制时复用原资源并保留其他页面的跳转
|
||||
// 入参: source 来源阅读器, indexes 来源页索引, at 插入位置, copyPage 是否同文档复制, options 导入选项
|
||||
// 返回: []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"})
|
||||
}
|
||||
|
||||
// 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 筛选指向导入页面的目录树并复用标准动作迁移
|
||||
// 返回: []byte 目录片段, error 错误信息
|
||||
func (m *editorPageImport) outlines() ([]byte, error) {
|
||||
|
||||
+100
-100
@@ -111,76 +111,6 @@ func (e *Editor) SetObjectActions(page int, ids []string, actions []Action) erro
|
||||
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 读取注解中唯一的点击跳转,书签目标保持原始动作
|
||||
// 入参: page 页面索引, id 注解标识
|
||||
// 返回: Action 跳转动作, error 错误信息
|
||||
@@ -203,36 +133,6 @@ func (e *Editor) AnnotationLink(page int, id string) (Action, error) {
|
||||
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 新增标准链接注解,区域位于页面毫米坐标,不检查目标是否可达
|
||||
// 入参: page 页面索引, box 点击区域, target 链接目标, creator 作者
|
||||
// 返回: 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 序列化标准跳转目标,不改写其他动作或命名空间
|
||||
// 入参: target 跳转目标
|
||||
// 返回: []byte 独立XML片段, error 错误信息
|
||||
|
||||
+54
-54
@@ -98,16 +98,6 @@ type ObjectCapabilities struct {
|
||||
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对象;新增空白页面始终可用
|
||||
type PageCapabilities struct {
|
||||
Insert bool
|
||||
@@ -159,6 +149,60 @@ func (r *Reader) Editor() (*Editor, error) {
|
||||
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 复用已验证的只读包索引,独立保存可变文档与读取缓存
|
||||
// 入参: 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
|
||||
}
|
||||
|
||||
// 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对象能否直接使用文档默认颜色空间
|
||||
// 返回: bool 是否为8位RGB文档
|
||||
func (e *Editor) sourceRGB() bool {
|
||||
@@ -484,19 +518,6 @@ func (e *Editor) loadSourcePage(index int) error {
|
||||
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 检查对象能力,在同次遍历中复用原容器排序检查
|
||||
// 入参: object 当前对象, orderable 原容器排序能力
|
||||
// 返回: ObjectCapabilities 操作能力
|
||||
@@ -822,27 +843,6 @@ func (e *Editor) editorFont(id string) (FontMetrics, error) {
|
||||
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 判断原字形能否在不使用回退字体的情况下度量
|
||||
// 入参: text 原文字对象
|
||||
// 返回: bool 是否可准确度量
|
||||
|
||||
+165
-165
@@ -207,6 +207,171 @@ func (e *Editor) AddDrawParam(draw DrawParam) (string, error) {
|
||||
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 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照
|
||||
// 入参: id 绘制参数标识, visited 已访问标识
|
||||
// 返回: *DrawParam 合并参数, error 错误信息
|
||||
@@ -316,32 +481,6 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
|
||||
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 仅替换请求的外观字段,保留内容、定位及未修改样式
|
||||
// 入参: object 原对象, style 待修改属性
|
||||
// 返回: GraphicObject 修改后的独立对象, error 错误信息
|
||||
@@ -439,75 +578,6 @@ func editorStyleColor(color, before *FillColor) *FillColor {
|
||||
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 校验样式来源,不分配资源或改写对象
|
||||
// 入参: source 有效外观快照
|
||||
// 返回: error 错误信息
|
||||
@@ -588,76 +658,6 @@ func editorStrokeScale(ctm string) float64 {
|
||||
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 校验图片边框的线型、圆角和绘制颜色
|
||||
// 入参: border 边框
|
||||
// 返回: error 错误信息
|
||||
|
||||
+44
-44
@@ -32,34 +32,6 @@ type MissingGlyphError struct {
|
||||
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 本地横向段落选项
|
||||
// Shape显式启用FontShaper,保存为标准TextCode和CGTransform,不改变阅读时的原文定位
|
||||
// Shaping提供可比较的塑形选项,非零值也会启用塑形,并随会话内文字编辑保留
|
||||
@@ -89,6 +61,22 @@ type TextStyle struct {
|
||||
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 原子更新同页文字样式,全部校验通过后提交一次撤销记录
|
||||
// 已知段落重新排版;原布局未知时替换字体和颜色保留坐标,不允许自动调整字号
|
||||
// 入参: page 页面索引, ids 文字对象标识, style 样式增量
|
||||
@@ -287,6 +275,34 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
||||
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 局部替换字符步进,保留未修改字符及末尾附加位移
|
||||
// 入参: value 原位移, old 原字符, replacement 替换字符, start 起点, end 终点, advance 字体步进
|
||||
// 返回: string 更新后的位移数组
|
||||
@@ -372,22 +388,6 @@ func (e *Editor) styleTextObjects(objects []GraphicObject, style TextStyle) ([]G
|
||||
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断行机会,过长词仅在字素边界折行,不丢弃空白
|
||||
// 入参: runes 段落字符, advances 含字距的字符步进, width 本地排版宽度, wrap 是否自动折行, spacing 附加字距, indent 首行缩进
|
||||
// 返回: [][2]int 各行的字符起止索引,左闭右开
|
||||
|
||||
+100
-100
@@ -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 对原有复杂对象仅更新变换,不重建其文字、画刷或其他局部属性
|
||||
// 入参: object 对象, dx、dy 页面位移, scale 缩放比例
|
||||
// 返回: GraphicObject 新对象, error 错误信息
|
||||
@@ -345,42 +445,6 @@ func transformObjectClips(clips *Clips, matrix Matrix) *Clips {
|
||||
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 更新图片副本的裁剪和局部坐标,不修改资源
|
||||
// 入参: image 图片对象副本, box 页面毫米坐标中的保留范围
|
||||
// 返回: error 错误信息
|
||||
@@ -420,25 +484,6 @@ func cropImageObject(image *ImageObject, box Box) error {
|
||||
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 将原始像素比例应用到当前图片框,适应时保留空白,填充时裁掉超出部分
|
||||
// 入参: obj 图片对象副本, mode 为contain或cover
|
||||
// 返回: error 错误信息
|
||||
@@ -484,51 +529,6 @@ func (e *Editor) fitImage(obj *ImageObject, mode string) error {
|
||||
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 使用统一精度序列化创作边界
|
||||
// 入参: box 矩形范围
|
||||
// 返回: string 标准Boundary属性值
|
||||
|
||||
+8
-8
@@ -20,14 +20,6 @@ import (
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和
|
||||
// 入参: sfnt 已解析的字体
|
||||
// 返回: []byte 独立SFNT数据
|
||||
func fontSFNTData(sfnt *font.SFNT) []byte {
|
||||
data, _ := serializeOTF(sfnt.Tables)
|
||||
return data
|
||||
}
|
||||
|
||||
// FixFontDataAggressive 激进修复字体数据
|
||||
// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据
|
||||
// 对显式字形映射补充私有字符cmap,便于按glyph id渲染
|
||||
@@ -59,6 +51,14 @@ func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, ma
|
||||
return fixed, newData, mapping, mc, err
|
||||
}
|
||||
|
||||
// fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和
|
||||
// 入参: sfnt 已解析的字体
|
||||
// 返回: []byte 独立SFNT数据
|
||||
func fontSFNTData(sfnt *font.SFNT) []byte {
|
||||
data, _ := serializeOTF(sfnt.Tables)
|
||||
return data
|
||||
}
|
||||
|
||||
// isBareCFFData 检查是否为CFF裸字体数据
|
||||
// 入参: data 字体数据
|
||||
// 返回: bool 是否为CFF裸数据
|
||||
|
||||
+23
-23
@@ -123,29 +123,6 @@ type resolvedFontResult struct {
|
||||
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 使用配置的字体后端解析资源,保留精确匹配与回退的区别
|
||||
// 入参: id 字体资源标识, exact 是否要求精确匹配
|
||||
// 返回: ResolvedFont 字体与来源, error 解析错误
|
||||
@@ -198,3 +175,26 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) {
|
||||
r.preparedFonts[id] = prepared
|
||||
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
@@ -26,6 +26,21 @@ import (
|
||||
"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字体格式
|
||||
// 入参: cffData CFF字体数据
|
||||
// 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息
|
||||
@@ -80,9 +95,6 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
|
||||
return data, mapping, err
|
||||
}
|
||||
|
||||
// cffDict 使用float64存储所有数值,以统一处理整数和实数
|
||||
type cffDict map[int][]float64
|
||||
|
||||
// normalizeCFFCharstrings 规范化Type2提示指令的等价编码,保留轮廓和提示数据
|
||||
// 入参: data CFF字体数据
|
||||
// 返回: []byte 标准化后的CFF数据, error 错误信息
|
||||
@@ -945,12 +957,6 @@ func readCFFLocalSubrs(data []byte, fdArrOff int, fdCount int) ([][][]byte, []cf
|
||||
return localSubrs, privateDicts
|
||||
}
|
||||
|
||||
// type2Operand Type2操作数
|
||||
type type2Operand struct {
|
||||
value int
|
||||
outStart int
|
||||
}
|
||||
|
||||
// inlineType2CharString 内联Type2 CharString子程序调用
|
||||
// 入参: data CharString数据, localSubrs 本地子程序, globalSubrs 全局子程序, depth 递归深度
|
||||
// 返回: []byte 内联后的CharString数据
|
||||
@@ -1719,9 +1725,3 @@ func getUnicodeFromName(name string) rune {
|
||||
}
|
||||
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",
|
||||
}
|
||||
@@ -18,8 +18,17 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// cmapSegment cmap表段结构
|
||||
// 字段: start 开始字符, end 结束字符, delta 增量, offset 偏移
|
||||
type cmapSegment struct {
|
||||
start, end uint16
|
||||
delta int16
|
||||
offset uint16
|
||||
}
|
||||
|
||||
// normalizeCmap 转换旧式双字节子表,保留各编码记录及原字形编号
|
||||
// 入参: data 字符映射表
|
||||
// 返回: []byte 等价字符映射表, error 子表错误
|
||||
@@ -120,3 +129,385 @@ func parseCmapFormat2(data []byte) (map[rune]uint16, error) {
|
||||
}
|
||||
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
@@ -60,26 +60,6 @@ func (f FontFile) Faces() ([]FontFace, error) {
|
||||
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,且不修改原文件
|
||||
// 入参: index 零起始字体索引,非集合文件只能为0
|
||||
// 返回: []byte 独立字体数据, error 格式或索引错误
|
||||
@@ -139,6 +119,26 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) {
|
||||
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 校验字体或集合头并读取字体数量
|
||||
// 入参: data OpenType数据
|
||||
// 返回: int 字体数量, error 格式错误
|
||||
|
||||
+188
-183
@@ -25,6 +25,18 @@ import (
|
||||
"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 内存字体文件
|
||||
type FontFile struct {
|
||||
Name string
|
||||
@@ -39,6 +51,67 @@ type FontFS struct {
|
||||
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 创建内存字体文件系统
|
||||
// 入参: fonts 字体文件列表
|
||||
// 返回: *FontFS 内存字体文件系统
|
||||
@@ -217,6 +290,121 @@ func (r *Reader) MatchFontFiles(files []string, ids ...string) ([]string, error)
|
||||
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 匹配指定样式的字体名称
|
||||
// 入参: names 字体名称列表, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []fontFileMatch 字体匹配列表
|
||||
@@ -315,18 +503,6 @@ func cleanFontName(name string) string {
|
||||
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 筛选字体文件候选
|
||||
// 入参: names 文件名列表, base 文件名提取函数
|
||||
// 返回: []fontFileCandidate 字体文件候选列表
|
||||
@@ -349,15 +525,6 @@ func fontFileCandidates(names []string, base func(string) string) []fontFileCand
|
||||
return files
|
||||
}
|
||||
|
||||
// fontPatternMatcher 字体文件匹配器
|
||||
type fontPatternMatcher struct {
|
||||
pattern string
|
||||
lowerPattern string
|
||||
stem string
|
||||
aliases []string
|
||||
priority int
|
||||
}
|
||||
|
||||
// newFontPatternMatcher 创建字体文件匹配器
|
||||
// 入参: pattern 匹配模式
|
||||
// 返回: fontPatternMatcher 字体文件匹配器
|
||||
@@ -483,22 +650,6 @@ func (m fontPatternMatcher) styleSuffixNormalized(name string) string {
|
||||
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 追加字体文件匹配结果
|
||||
// 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
@@ -52,6 +52,20 @@ type FontInfo struct {
|
||||
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声明的字体列表
|
||||
// 返回: []Font 字体列表, error 错误信息
|
||||
func (r *Reader) Fonts() ([]Font, error) {
|
||||
@@ -104,48 +118,6 @@ func (r *Reader) FontData(id string) ([]byte, error) {
|
||||
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字体诊断信息
|
||||
// 返回: []FontInfo 字体诊断列表, error 错误信息
|
||||
func (r *Renderer) FontInfos() ([]FontInfo, error) {
|
||||
@@ -158,14 +130,6 @@ func (r *Renderer) FontInfos() ([]FontInfo, error) {
|
||||
return scanner.Infos()
|
||||
}
|
||||
|
||||
// FontInfoScanner 逐页统计字体用量,不保留页面图元,扫描期间不得修改阅读器或渲染器配置
|
||||
type FontInfoScanner struct {
|
||||
renderer *Renderer
|
||||
doc *Document
|
||||
index int
|
||||
usage map[string]int
|
||||
}
|
||||
|
||||
// ScanFontInfos 创建分步字体诊断,页面读取失败的处理与FontInfos一致
|
||||
// 返回: *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))
|
||||
}
|
||||
|
||||
// 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 获取字体诊断文档结构
|
||||
// 返回: *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
|
||||
}
|
||||
|
||||
// fontUsagePage 逐图元统计字体的页面解码器
|
||||
type fontUsagePage struct {
|
||||
renderer *Renderer
|
||||
usage map[string]int
|
||||
}
|
||||
|
||||
// readPageFontUsage 读取页面字体用量,不保留页面图元
|
||||
// 入参: page 页面引用
|
||||
// 返回: map[string]int 字体使用次数, error 错误信息
|
||||
@@ -379,26 +399,6 @@ func (r *Renderer) readPageFontUsage(page Page) (map[string]int, error) {
|
||||
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 逐个读取图元并复用字体统计规则
|
||||
// 入参: d XML解码器, start 容器起始节点, defaults 默认绘制参数
|
||||
// 返回: error 错误信息
|
||||
|
||||
+25
-25
@@ -20,15 +20,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fontMatchRule 字体匹配规则
|
||||
type fontMatchRule struct {
|
||||
Keys []string
|
||||
Names []string
|
||||
Files []string
|
||||
System string
|
||||
NoSyntheticBold bool
|
||||
}
|
||||
|
||||
const (
|
||||
fontMatchNone = iota
|
||||
fontMatchExact
|
||||
@@ -114,22 +105,13 @@ var fontMatchRules = normalizeFontMatchRules([]fontMatchRule{
|
||||
|
||||
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
|
||||
|
||||
// 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
|
||||
// fontMatchRule 字体匹配规则
|
||||
type fontMatchRule struct {
|
||||
Keys []string
|
||||
Names []string
|
||||
Files []string
|
||||
System string
|
||||
NoSyntheticBold bool
|
||||
}
|
||||
|
||||
// FontNormalizeName 规范化字体名称
|
||||
@@ -160,6 +142,24 @@ func FontSystemNames(names ...string) []string {
|
||||
return fontSystemNames(names...)
|
||||
}
|
||||
|
||||
// normalizeFontMatchRules 预处理固定字体规则的匹配名称,保留候选名称及顺序
|
||||
// 入参: rules 字体匹配规则
|
||||
// 返回: []fontMatchRule 预处理后的规则
|
||||
func normalizeFontMatchRules(rules []fontMatchRule) []fontMatchRule {
|
||||
for i := range rules {
|
||||
rule := &rules[i]
|
||||
keys := append(append(rule.Keys, rule.Names...), rule.System)
|
||||
rule.Keys = nil
|
||||
for _, key := range keys {
|
||||
key = fontNormalizeName(key)
|
||||
if key != "" && !slices.Contains(rule.Keys, key) {
|
||||
rule.Keys = append(rule.Keys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// fontNormalizeName 规范化字体名称
|
||||
// 入参: name 字体名称
|
||||
// 返回: string 规范化后的字体名称
|
||||
|
||||
+3
-3
@@ -44,6 +44,9 @@ type fontShaper struct {
|
||||
shapeBuffer *harfbuzz.Buffer
|
||||
}
|
||||
|
||||
// shapingFace 固定双向排版的字体来源,不隐式替换字体
|
||||
type shapingFace struct{ face *typefont.Face }
|
||||
|
||||
// ShapeText 使用默认选项塑形单行文字,不改动原文定位
|
||||
// 入参: value 单行原文, size 毫米字号
|
||||
// 返回: []ShapedGlyph 定位字形, error 塑形错误
|
||||
@@ -152,9 +155,6 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// shapingFace 固定双向排版的字体来源,不隐式替换字体
|
||||
type shapingFace struct{ face *typefont.Face }
|
||||
|
||||
// ResolveFace 返回指定字体
|
||||
// 入参: char 字符
|
||||
// 返回: *typefont.Face 字体
|
||||
|
||||
@@ -29,15 +29,21 @@ import (
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// fontSourceKind 字体来源类型
|
||||
type fontSourceKind uint8
|
||||
|
||||
const (
|
||||
fontSourceFile fontSourceKind = iota
|
||||
fontSourceFS
|
||||
fontSourceSystem
|
||||
)
|
||||
|
||||
// systemFontIndex 串行初始化系统字体索引,不注册绘图后端
|
||||
var systemFontIndex struct {
|
||||
sync.Mutex
|
||||
fonts *font.SystemFonts
|
||||
}
|
||||
|
||||
// fontSourceKind 字体来源类型
|
||||
type fontSourceKind uint8
|
||||
|
||||
// fontSource 字体来源
|
||||
type fontSource struct {
|
||||
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)}
|
||||
}
|
||||
|
||||
// systemFontIndex 串行初始化系统字体索引,不注册绘图后端
|
||||
var systemFontIndex struct {
|
||||
sync.Mutex
|
||||
fonts *font.SystemFonts
|
||||
}
|
||||
|
||||
// readFontSource 读取并拆出独立字体,保留集合中的名称和样式选择
|
||||
// 入参: source 字体来源, definition 字体定义
|
||||
// 返回: []byte 字体数据, error 读取或集合错误
|
||||
|
||||
+40
-40
@@ -23,6 +23,24 @@ import (
|
||||
"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字形与子程序
|
||||
type type1Program struct {
|
||||
glyphs []type1Glyph
|
||||
@@ -42,6 +60,28 @@ type type1Scanner struct {
|
||||
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字体的字形程序及子程序
|
||||
// 入参: data PFA或PFB字体数据
|
||||
// 返回: type1Program 字形和子程序, error 错误信息
|
||||
@@ -303,28 +343,6 @@ func type1BinaryStart(token string) bool {
|
||||
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程序
|
||||
// 入参: glyph 字形数据
|
||||
// 返回: []byte Type2字形程序, error 错误信息
|
||||
@@ -638,24 +656,6 @@ func (s *type1Outline) component(code, x, y float64) error {
|
||||
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字形程序中建立新轮廓
|
||||
// 返回: error 错误信息
|
||||
func (s *type1Outline) move() error {
|
||||
|
||||
+10
-400
@@ -21,6 +21,16 @@ import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// otfTableRecord OTF表记录结构
|
||||
// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据
|
||||
type otfTableRecord struct {
|
||||
tag string
|
||||
checksum uint32
|
||||
offset int
|
||||
length int
|
||||
data []byte
|
||||
}
|
||||
|
||||
// extractCollectionFont 提取集合中的单个OpenType字体,保留表内容并重新计算校验和
|
||||
// 入参: data 集合数据, index 字体索引
|
||||
// 返回: []byte 单字体数据, error 错误信息
|
||||
@@ -81,13 +91,6 @@ func fontFileTables(data []byte, index int) (map[string][]byte, error) {
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// packedGlyphRune 获取包装字体字符
|
||||
// 入参: gid 字形ID
|
||||
// 返回: rune 包装字体字符
|
||||
func packedGlyphRune(gid uint16) rune {
|
||||
return 0xF0000 + rune(gid)
|
||||
}
|
||||
|
||||
// calcTableChecksum 计算字体表校验和
|
||||
// 入参: data 表数据
|
||||
// 返回: uint32 校验和
|
||||
@@ -109,165 +112,6 @@ func calcTableChecksum(data []byte) uint32 {
|
||||
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表
|
||||
// 入参: unitsPerEm 每em单位数
|
||||
// 返回: []byte head表数据
|
||||
@@ -436,240 +280,6 @@ func buildHmtxTable(widths []uint16) []byte {
|
||||
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字体结构
|
||||
// 入参: tables 表数据映射
|
||||
// 返回: []byte 完整字体数据, error 错误信息
|
||||
|
||||
+27
-27
@@ -22,6 +22,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IdentityMatrix 单位矩阵
|
||||
var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0}
|
||||
|
||||
// Box 矩形区域
|
||||
type Box struct {
|
||||
X, Y, W, H float64
|
||||
@@ -32,11 +35,9 @@ type Point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
// geometryEqual 比较毫米坐标,使用固定绝对误差,不受绘图库全局精度影响
|
||||
// 入参: a、b 待比较的坐标
|
||||
// 返回: bool 是否在容差内相等
|
||||
func geometryEqual(a, b float64) bool {
|
||||
return math.Abs(a-b) <= 1e-10
|
||||
// Matrix 2D仿射变换矩阵
|
||||
type Matrix struct {
|
||||
a, b, c, d, e, f float64
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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构造矩阵,不经过文本格式化
|
||||
// 入参: values 矩阵分量
|
||||
// 返回: 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))
|
||||
}
|
||||
|
||||
// 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 求逆矩阵
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// 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 解析浮点数数组
|
||||
// 入参: s 字符串
|
||||
// 返回: []float64 浮点数数组
|
||||
|
||||
+24
-24
@@ -19,9 +19,6 @@ import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// GeometryOperation 指定路径填充区域的布尔运算
|
||||
type GeometryOperation uint8
|
||||
|
||||
const (
|
||||
GeometryIntersect GeometryOperation = iota
|
||||
GeometryUnion
|
||||
@@ -29,6 +26,9 @@ const (
|
||||
GeometryXor
|
||||
)
|
||||
|
||||
// GeometryOperation 指定路径填充区域的布尔运算
|
||||
type GeometryOperation uint8
|
||||
|
||||
// StrokeOptions 描述描边参数,长度单位为毫米
|
||||
type StrokeOptions struct {
|
||||
// Width为正有限描边宽度
|
||||
@@ -64,27 +64,6 @@ type GeometryCurves interface {
|
||||
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 返回当前几何后端,不隐式恢复默认实现
|
||||
// 返回: GeometryBackend 几何后端, error 未配置错误
|
||||
func (r *Renderer) Geometry() (GeometryBackend, error) {
|
||||
@@ -118,6 +97,27 @@ func (r *Renderer) PathOutline(object PathObject) (string, error) {
|
||||
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 构建页面坐标矩形,保留输入精度
|
||||
// 入参: box 矩形范围
|
||||
// 返回: GeometryPath 闭合路径
|
||||
|
||||
+19
-19
@@ -20,13 +20,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GeometryPath 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标
|
||||
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
||||
type GeometryPath []GeometrySegment
|
||||
|
||||
// GeometryVerb 表示几何路径指令
|
||||
type GeometryVerb uint8
|
||||
|
||||
const (
|
||||
GeometryMove GeometryVerb = iota
|
||||
GeometryLine
|
||||
@@ -36,6 +29,13 @@ const (
|
||||
GeometryClose
|
||||
)
|
||||
|
||||
// GeometryPath 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标
|
||||
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
||||
type GeometryPath []GeometrySegment
|
||||
|
||||
// GeometryVerb 表示几何路径指令
|
||||
type GeometryVerb uint8
|
||||
|
||||
// GeometrySegment 保存路径端点、贝塞尔控制点或端点式椭圆弧参数
|
||||
// 弧线Rotation单位为度,Sweep为true表示顺时针,Large表示大弧
|
||||
type GeometrySegment struct {
|
||||
@@ -117,6 +117,18 @@ func ParseGeometryPath(data string) (GeometryPath, error) {
|
||||
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 解析对象路径并由当前几何实现应用页面变换
|
||||
// 入参: geometry 几何后端, object 路径对象
|
||||
// 返回: GeometryPath 页面路径, error 参数或变换错误
|
||||
@@ -156,18 +168,6 @@ func closedGeometry(path GeometryPath) GeometryPath {
|
||||
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
|
||||
// 入参: svg 是否使用SVG指令
|
||||
// 返回: string 路径数据, error 不支持的路径指令
|
||||
|
||||
+35
-35
@@ -20,41 +20,6 @@ import (
|
||||
"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 精确变换控制点和椭圆轴,退化弧沿投影极值分段保留折返
|
||||
// 入参: matrix 仿射变换
|
||||
// 返回: GeometryPath 独立路径, error 无效路径或数值溢出
|
||||
@@ -158,3 +123,38 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
|
||||
}
|
||||
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
@@ -29,6 +29,107 @@ import (
|
||||
_ "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 解码图片数据
|
||||
// 入参: data 图片数据
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// 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 保留十六位颜色和透明度,不经八位图像中转
|
||||
// 入参: original 原始图像, source 已解码的像素源
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// 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
@@ -43,20 +43,6 @@ func NewEncodedImage(data []byte) (*EncodedImage, error) {
|
||||
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 返回只读原始编码,调用方不得修改
|
||||
// 返回: []byte 原始编码
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 复用原始图片资源,不依赖输出后端
|
||||
// 入参: resPath 图片资源路径
|
||||
// 返回: image.Image 图片对象, error 读取错误
|
||||
|
||||
+68
-68
@@ -19,6 +19,15 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// graphicObjectTarget 图形对象集合
|
||||
type graphicObjectTarget struct {
|
||||
objects *[]GraphicObject
|
||||
text *[]TextObject
|
||||
path *[]PathObject
|
||||
image *[]ImageObject
|
||||
composite *[]CompositeGraphicUnit
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// graphicObjectTarget 图形对象集合
|
||||
type graphicObjectTarget struct {
|
||||
objects *[]GraphicObject
|
||||
text *[]TextObject
|
||||
path *[]PathObject
|
||||
image *[]ImageObject
|
||||
composite *[]CompositeGraphicUnit
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 解析图形对象
|
||||
@@ -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 解析图层子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
@@ -214,30 +264,6 @@ func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error {
|
||||
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 解析复合图元子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
@@ -275,15 +301,6 @@ func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartEleme
|
||||
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 解析注释外观子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
@@ -304,14 +321,6 @@ func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error
|
||||
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 解析图案单元内容子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
@@ -332,15 +341,6 @@ func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) er
|
||||
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属性值
|
||||
// 入参: start 起始节点, name 属性名
|
||||
// 返回: string 属性值
|
||||
|
||||
+20
-20
@@ -21,6 +21,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PatternPaint 底纹画刷
|
||||
type PatternPaint struct {
|
||||
*Pattern
|
||||
Color FillColor
|
||||
Alpha *int
|
||||
}
|
||||
|
||||
// ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA
|
||||
// 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度
|
||||
// 返回: 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))
|
||||
}
|
||||
|
||||
// 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 解析带透明度的颜色
|
||||
// 入参: value 颜色值, index 调色板索引, space 颜色空间标识, alpha 透明度
|
||||
// 返回: color.Color 颜色对象
|
||||
@@ -204,13 +224,6 @@ func (r *Renderer) parseStrokeColor(strokeColor *StrokeColor) color.Color {
|
||||
return r.parseFillColor((*FillColor)(strokeColor))
|
||||
}
|
||||
|
||||
// PatternPaint 底纹画刷
|
||||
type PatternPaint struct {
|
||||
*Pattern
|
||||
Color FillColor
|
||||
Alpha *int
|
||||
}
|
||||
|
||||
// parsePatternPaint 解析底纹画刷
|
||||
// 入参: fill 填充颜色节点
|
||||
// 返回: *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))
|
||||
}
|
||||
|
||||
// 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位灰阶内
|
||||
// 入参: segments 原始颜色分段, alpha 对象透明度
|
||||
// 返回: []ColorStop 仅供绘制的颜色节点,不改变原始分段
|
||||
|
||||
+31
-31
@@ -57,6 +57,37 @@ type MeshShading struct {
|
||||
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 返回方向一致的三角形轮廓,供绘制裁剪和对象度量共用
|
||||
// 返回: GeometryPath 非零绕数填充轮廓
|
||||
func (m *MeshShading) outline() GeometryPath {
|
||||
@@ -179,37 +210,6 @@ func meshTriangleIndices(points []ShdPoint, columns, extend int) ([][3]int, erro
|
||||
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
|
||||
// 入参: space 颜色模型, values 分量
|
||||
// 返回: float64 红、绿、蓝分量
|
||||
|
||||
@@ -16,15 +16,6 @@ package ofdgo
|
||||
|
||||
import "image/color"
|
||||
|
||||
// ColorStop 保存渐变区间位置及预乘RGBA颜色节点
|
||||
type ColorStop struct {
|
||||
Offset float64
|
||||
Color color.RGBA
|
||||
}
|
||||
|
||||
// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变、图案和网格渐变
|
||||
type PaintKind uint8
|
||||
|
||||
const (
|
||||
PaintNone PaintKind = iota
|
||||
PaintSolid
|
||||
@@ -34,6 +25,15 @@ const (
|
||||
PaintMesh
|
||||
)
|
||||
|
||||
// ColorStop 保存渐变区间位置及预乘RGBA颜色节点
|
||||
type ColorStop struct {
|
||||
Offset float64
|
||||
Color color.RGBA
|
||||
}
|
||||
|
||||
// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变、图案和网格渐变
|
||||
type PaintKind uint8
|
||||
|
||||
// Paint 保存后端无关的颜色、渐变和图案,不修改源节点
|
||||
type Paint struct {
|
||||
Kind PaintKind
|
||||
|
||||
+13
-13
@@ -204,19 +204,6 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
||||
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,转换阶段失败时不写入目标
|
||||
// 入参: ctx 取消上下文, source PDF数据, size 字节数, output OFD输出, options 转换选项
|
||||
// 返回: PDFImportReport 转换统计, error 错误信息
|
||||
@@ -247,6 +234,19 @@ func ConvertPDF(ctx context.Context, source io.ReaderAt, size int64, output io.W
|
||||
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毫米坐标
|
||||
// 入参: page PDF页面
|
||||
// 返回: pdfgo.Matrix 坐标变换, float64 页面宽度, float64 页面高度
|
||||
|
||||
+30
-30
@@ -21,6 +21,36 @@ import (
|
||||
"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高洛德渐变,保留顶点和覆盖顺序
|
||||
// 入参: paint PDF网格画刷, box 对象边界
|
||||
// 返回: *FillColor OFD渐变, error 不可线性表达的颜色或坐标错误
|
||||
@@ -59,36 +89,6 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
||||
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 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内
|
||||
// 入参: mesh 原始曲面
|
||||
// 返回: []pdfMeshTriangle 细分结果, error 取消或坐标错误
|
||||
|
||||
+19
-19
@@ -21,6 +21,20 @@ import (
|
||||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
RasterMove RasterVerb = iota
|
||||
RasterLine
|
||||
RasterQuad
|
||||
RasterCubic
|
||||
RasterClose
|
||||
)
|
||||
|
||||
const (
|
||||
RasterLinear RasterGradientKind = iota
|
||||
RasterRadial
|
||||
RasterMesh
|
||||
)
|
||||
|
||||
// RasterPage 已解析的光栅绘制页面,单位为毫米,页面坐标原点在左上角
|
||||
// DPI决定像素尺寸和编译时的字形、描边精度,修改DPI后应重新编译
|
||||
// 页面及其引用资源在渲染期间只读,不用于重建或保存OFD
|
||||
@@ -39,14 +53,6 @@ type RasterPoint struct{ X, Y float64 }
|
||||
// RasterVerb 路径指令
|
||||
type RasterVerb uint8
|
||||
|
||||
const (
|
||||
RasterMove RasterVerb = iota
|
||||
RasterLine
|
||||
RasterQuad
|
||||
RasterCubic
|
||||
RasterClose
|
||||
)
|
||||
|
||||
// RasterSegment 路径片段,RasterMove和RasterLine使用End,RasterQuad使用Control1和End
|
||||
type RasterSegment struct {
|
||||
Verb RasterVerb
|
||||
@@ -78,12 +84,6 @@ type RasterPaint struct {
|
||||
// RasterGradientKind 渐变类型
|
||||
type RasterGradientKind uint8
|
||||
|
||||
const (
|
||||
RasterLinear RasterGradientKind = iota
|
||||
RasterRadial
|
||||
RasterMesh
|
||||
)
|
||||
|
||||
// RasterGradient 局部坐标下的轴向、双圆径向或三角网格渐变
|
||||
// 轴向及径向渐变的Spread为空时延续边界颜色,网格渐变使用Mesh
|
||||
type RasterGradient struct {
|
||||
@@ -116,11 +116,6 @@ func (p *RasterPage) PixelSize() (int, int, error) {
|
||||
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 将局部点映射到目标坐标
|
||||
// 入参: p 局部点
|
||||
// 返回: RasterPoint 目标点
|
||||
@@ -189,6 +184,11 @@ func (g *RasterGradient) At(x, y float64) color.RGBA {
|
||||
return g.colorAt(t)
|
||||
}
|
||||
|
||||
// rasterPositive 判断正有限数
|
||||
// 入参: v 待检查数值
|
||||
// 返回: bool 是否为正有限数
|
||||
func rasterPositive(v float64) bool { return v > 0 && !math.IsInf(v, 0) && !math.IsNaN(v) }
|
||||
|
||||
// rasterRadialDomain 求延伸区间内半径非负的最后一个双圆参数
|
||||
// 入参: a、b、c 方程系数, radius 起始半径, delta 半径变化, extend 延伸标志
|
||||
// 返回: float64 参数, bool 是否覆盖采样点
|
||||
|
||||
+12
-12
@@ -22,6 +22,18 @@ import (
|
||||
"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 按页面变换对图像执行高质量采样
|
||||
// 入参: dst 目标图像, src 源图像, m 像素坐标变换
|
||||
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))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -56,6 +56,12 @@ type rasterPreparationState struct {
|
||||
scene RasterScene
|
||||
}
|
||||
|
||||
// rasterScene 复用后端无关指令,图片保持只读引用
|
||||
type rasterScene struct {
|
||||
backend RasterBackend
|
||||
page *RasterPage
|
||||
}
|
||||
|
||||
// PreparePage 编译并准备当前配置的独立页面快照,按后端能力复用未修改指令
|
||||
// 每次重新解释源页面,按内容使模板、签章、字体及样式派生数据失效
|
||||
// 入参: page 源页面
|
||||
@@ -100,12 +106,6 @@ func PrepareRasterScene(backend RasterBackend, page *RasterPage) (RasterScene, e
|
||||
return &rasterScene{backend: backend, page: cloneRasterPage(page)}, nil
|
||||
}
|
||||
|
||||
// rasterScene 复用后端无关指令,图片保持只读引用
|
||||
type rasterScene struct {
|
||||
backend RasterBackend
|
||||
page *RasterPage
|
||||
}
|
||||
|
||||
// Render 绘制固定快照
|
||||
// 返回: image.Image 独立图像, error 绘制错误
|
||||
func (s *rasterScene) Render() (image.Image, error) { return s.backend.Render(s.page) }
|
||||
|
||||
+362
-362
@@ -63,155 +63,6 @@ func (r *Reader) Close() error {
|
||||
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 获取主文档结构
|
||||
// 返回: *Document 文档结构, error 错误信息
|
||||
func (r *Reader) Doc() (*Document, error) {
|
||||
@@ -247,80 +98,6 @@ func (r *Reader) Doc() (*Document, error) {
|
||||
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 获取页面内容
|
||||
// 每次返回独立的页面内容,修改后可传入Renderer重绘,不会写回原文件
|
||||
// 入参: page 页面对象
|
||||
@@ -329,36 +106,6 @@ func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
||||
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 获取当前文档总页数
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// 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 获取资源的完整路径
|
||||
// 入参: resLink 资源链接
|
||||
// 返回: string 完整路径
|
||||
@@ -519,21 +172,6 @@ func (r *Reader) ResData(resLink string) ([]byte, error) {
|
||||
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版本号
|
||||
// 返回: string 版本号
|
||||
func (r *Reader) Version() string {
|
||||
@@ -728,3 +366,365 @@ func (r *Reader) Extensions() ([]Extension, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -16,6 +16,12 @@ package ofdgo
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ObjectContour 页面坐标中的填充轮廓,Path为SVG路径,EvenOdd表示奇偶填充
|
||||
type ObjectContour struct {
|
||||
Path string `json:"path"`
|
||||
EvenOdd bool `json:"evenOdd,omitempty"`
|
||||
}
|
||||
|
||||
// ObjectBounds 获取文字、路径、图片或复合对象在页面坐标中的轴对齐范围,不修改对象
|
||||
// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素
|
||||
// 底纹按填充或描边轮廓度量,不展开图案单元
|
||||
@@ -27,12 +33,6 @@ func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, er
|
||||
return result.Bounds, err
|
||||
}
|
||||
|
||||
// ObjectContour 页面坐标中的填充轮廓,Path为SVG路径,EvenOdd表示奇偶填充
|
||||
type ObjectContour struct {
|
||||
Path string `json:"path"`
|
||||
EvenOdd bool `json:"evenOdd,omitempty"`
|
||||
}
|
||||
|
||||
// AnnotationGeometry 获取注解外观的实际内容轮廓,不将Appearance容器边界视为绘制内容
|
||||
// 链接保留透明对象定义的交互区域,图片按几何范围度量而不解码透明像素
|
||||
// 入参: annotation 注解
|
||||
|
||||
+15
-15
@@ -32,15 +32,10 @@ type semanticCompiler struct {
|
||||
patterns *renderCache[patternCellKey, []RasterCommand]
|
||||
}
|
||||
|
||||
// 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
|
||||
// semanticStyle 保存已继承的路径颜色和描边参数
|
||||
type semanticStyle struct {
|
||||
fill, stroke *FillColor
|
||||
options StrokeOptions
|
||||
}
|
||||
|
||||
// DrawObject 解释叶子对象,不调用任何具体页面编译器
|
||||
@@ -64,6 +59,17 @@ func (c *semanticCompiler) DrawObject(object *GraphicObject, state RenderState)
|
||||
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 转换光栅曲线,仅在输出边界展开椭圆弧
|
||||
// 入参: path 页面路径
|
||||
// 返回: []RasterSegment 光栅路径, error 能力或路径错误
|
||||
@@ -268,12 +274,6 @@ func semanticGradient(paint Paint, matrix Matrix) (*RasterGradient, Matrix) {
|
||||
return g, matrix
|
||||
}
|
||||
|
||||
// semanticStyle 保存已继承的路径颜色和描边参数
|
||||
type semanticStyle struct {
|
||||
fill, stroke *FillColor
|
||||
options StrokeOptions
|
||||
}
|
||||
|
||||
// objectStyle 合并继承与对象样式,不修改共享绘制参数
|
||||
// 入参: object 路径样式, defaults 继承样式, linear 组合变换
|
||||
// 返回: semanticStyle 解析样式
|
||||
|
||||
@@ -22,6 +22,13 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
// DrawStamp 保留签章图片和嵌套OFD的绘制顺序,不混入正文搜索
|
||||
// 入参: stamp 印章
|
||||
// 返回: error 印章解析或编译错误
|
||||
func (c *semanticCompiler) DrawStamp(stamp Stamp) error {
|
||||
return c.sharedStamp(stamp, (*semanticCompiler).drawStampContent)
|
||||
}
|
||||
|
||||
// imageObject 共用图片定位、裁剪和边框,不在度量时读取像素
|
||||
// 入参: object 图片对象, state 继承状态
|
||||
// 返回: error 资源或几何错误
|
||||
@@ -135,13 +142,6 @@ func (c *semanticCompiler) addImage(img image.Image, matrix Matrix, clip *Geomet
|
||||
return nil
|
||||
}
|
||||
|
||||
// DrawStamp 保留签章图片和嵌套OFD的绘制顺序,不混入正文搜索
|
||||
// 入参: stamp 印章
|
||||
// 返回: error 印章解析或编译错误
|
||||
func (c *semanticCompiler) DrawStamp(stamp Stamp) error {
|
||||
return c.sharedStamp(stamp, (*semanticCompiler).drawStampContent)
|
||||
}
|
||||
|
||||
// drawStampContent 解释未缓存的签章内容,保持嵌套文档及裁剪语义
|
||||
// 入参: stamp 印章
|
||||
// 返回: error 印章解析或编译错误
|
||||
|
||||
+59
-59
@@ -87,30 +87,6 @@ func (r *Renderer) RenderTo(page *PageContent, writer io.Writer, format string)
|
||||
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,文件编号按总页数补零,出错时调用方应丢弃输出
|
||||
// 入参: writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt, indices 零基页面索引,省略则全部,按原页序去重
|
||||
// 返回: error 错误信息
|
||||
@@ -203,16 +179,6 @@ func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error
|
||||
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 渲染为光栅图
|
||||
// 入参: page 页面内容
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: error 错误信息
|
||||
@@ -363,6 +304,65 @@ func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) e
|
||||
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后端
|
||||
// 入参: pages 页面列表, writer 输出流, progress 页面处理进度
|
||||
// 返回: error 错误信息
|
||||
|
||||
@@ -39,6 +39,15 @@ type stampContentState struct {
|
||||
// stampContentStateKey 隔离渲染器私有的共享签章数据
|
||||
type stampContentStateKey struct{}
|
||||
|
||||
// patternCellKey 区分共享底纹、颜色、透明度和影响单元语义的页面变换
|
||||
type patternCellKey struct {
|
||||
pattern *Pattern
|
||||
color, space string
|
||||
index, alpha int
|
||||
colorAlpha int
|
||||
matrix [6]float64
|
||||
}
|
||||
|
||||
// resetFonts 使包含文字的签章编译结果随字体配置失效
|
||||
func (s *stampContentState) resetFonts() {
|
||||
s.cache = renderCache[stampContentKey, []RasterCommand]{limit: 16 << 20}
|
||||
@@ -88,15 +97,6 @@ func (c *semanticCompiler) sharedStamp(stamp Stamp, compile func(*semanticCompil
|
||||
return nil
|
||||
}
|
||||
|
||||
// patternCellKey 区分共享底纹、颜色、透明度和影响单元语义的页面变换
|
||||
type patternCellKey struct {
|
||||
pattern *Pattern
|
||||
color, space string
|
||||
index, alpha int
|
||||
colorAlpha int
|
||||
matrix [6]float64
|
||||
}
|
||||
|
||||
// patternCell 复用同次编译中相同底纹单元,不缓存外部对象的裁剪
|
||||
// 入参: pattern 底纹画刷, matrix 单元到页面矩阵
|
||||
// 返回: []RasterCommand 只读单元指令, error 编译错误
|
||||
|
||||
+21
-21
@@ -14,6 +14,15 @@
|
||||
|
||||
package ofdgo
|
||||
|
||||
const (
|
||||
sceneObject sceneCommandKind = iota
|
||||
sceneStamp
|
||||
sceneBeginObject
|
||||
sceneBeginAnnotation
|
||||
sceneEnd
|
||||
sceneSkip
|
||||
)
|
||||
|
||||
// PageScene 保存后端无关的页面语义与绘制顺序,不提前展开文字或重新编码图片
|
||||
// 场景借用源页面及资源的只读引用,编辑文档后应重新构建
|
||||
type PageScene struct {
|
||||
@@ -35,14 +44,8 @@ type sceneCommand struct {
|
||||
// sceneCommandKind 区分场景中的绘制和分组事件
|
||||
type sceneCommandKind uint8
|
||||
|
||||
const (
|
||||
sceneObject sceneCommandKind = iota
|
||||
sceneStamp
|
||||
sceneBeginObject
|
||||
sceneBeginAnnotation
|
||||
sceneEnd
|
||||
sceneSkip
|
||||
)
|
||||
// sceneRecorder 收集公共遍历事件,不解释绘制外观
|
||||
type sceneRecorder PageScene
|
||||
|
||||
// CompileScene 展开页面结构,保留原文、字形索引、动作和对象标识
|
||||
// 不提前绘制文字或图片,裁剪轮廓按几何后端需求解析
|
||||
@@ -102,19 +105,6 @@ func (s *PageScene) Walk(visitor PageVisitor) error {
|
||||
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 保留源对象及继承状态
|
||||
// 入参: object 源对象, state 继承状态
|
||||
// 返回: error 固定为空
|
||||
@@ -155,3 +145,13 @@ func (s *sceneRecorder) EndObject() { s.commands = append(s.commands, sceneComma
|
||||
func (s *sceneRecorder) SkipObjects(count int) {
|
||||
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
@@ -44,160 +44,6 @@ type glyphOutlineKey struct {
|
||||
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 = ©
|
||||
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 定位文字、字形与搜索范围,不依赖页面输出后端
|
||||
// 入参: object 文字对象, state 继承状态
|
||||
// 返回: *PositionedText 定位结果, error 字体或几何错误
|
||||
@@ -403,6 +249,160 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
|
||||
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 = ©
|
||||
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 以公共几何语义收集文字选择区域,保留旋转和斜切
|
||||
// 入参: geometry 几何后端, start 起始字符, end 结束字符, bounds 局部范围, matrix 页面变换, clip 页面裁剪
|
||||
// 返回: error 几何错误
|
||||
|
||||
@@ -90,6 +90,13 @@ func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
|
||||
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 访问注解外观并保持独立分组和定位
|
||||
// 入参: annotation 注解, visitor 页面访问器
|
||||
// 返回: error 遍历或访问器错误
|
||||
@@ -149,13 +156,6 @@ func (r *Renderer) walkLayers(layers []Layer, order int, visitor PageVisitor) er
|
||||
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 访问对象并传递当前资源引用链
|
||||
// 入参: object 源对象, state 继承状态, visitor 页面访问器, references 当前资源引用链
|
||||
// 返回: error 遍历或访问器错误
|
||||
|
||||
+22
-22
@@ -30,18 +30,16 @@ type CanvasBackend struct {
|
||||
ColorSpace canvas.ColorSpace
|
||||
}
|
||||
|
||||
// init 注册Canvas适配器和默认组合
|
||||
func init() {
|
||||
if err := RegisterRenderBackend("canvas", defaultRenderBackends); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// rasterStrokeGradientCanvas 保留页面坐标描边的局部渐变定位
|
||||
type rasterStrokeGradientCanvas struct {
|
||||
rasterGradientCanvas
|
||||
inverse RasterMatrix
|
||||
}
|
||||
|
||||
// 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}
|
||||
// rasterGradientCanvas 将公共渐变交给Canvas采样,不改变OFD周期语义
|
||||
type rasterGradientCanvas struct {
|
||||
gradient *RasterGradient
|
||||
space canvas.ColorSpace
|
||||
}
|
||||
|
||||
// Name 返回后端标识
|
||||
@@ -182,12 +180,6 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// rasterStrokeGradientCanvas 保留页面坐标描边的局部渐变定位
|
||||
type rasterStrokeGradientCanvas struct {
|
||||
rasterGradientCanvas
|
||||
inverse RasterMatrix
|
||||
}
|
||||
|
||||
// At 返回原局部坐标的描边渐变颜色
|
||||
// 入参: x 页面横坐标, y 页面纵坐标
|
||||
// 返回: color.RGBA 预乘颜色
|
||||
@@ -204,12 +196,6 @@ func (g rasterStrokeGradientCanvas) SetColorSpace(space canvas.ColorSpace) canva
|
||||
return g
|
||||
}
|
||||
|
||||
// rasterGradientCanvas 将公共渐变交给Canvas采样,不改变OFD周期语义
|
||||
type rasterGradientCanvas struct {
|
||||
gradient *RasterGradient
|
||||
space canvas.ColorSpace
|
||||
}
|
||||
|
||||
// At 返回局部坐标的预乘颜色
|
||||
// 入参: x 横坐标, y 纵坐标
|
||||
// 返回: color.RGBA 渐变颜色
|
||||
@@ -236,3 +222,17 @@ func (g rasterGradientCanvas) SetColorSpace(space canvas.ColorSpace) canvas.Grad
|
||||
}
|
||||
return rasterGradientCanvas{gradient: ©}
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
@@ -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 合并文字字形轮廓
|
||||
// 入参: text 文字, m 变换矩阵
|
||||
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 变换矩阵
|
||||
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 构建对象裁剪路径并应用父级变换
|
||||
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
|
||||
// 返回: *canvas.Path 路径对象
|
||||
|
||||
@@ -22,6 +22,19 @@ import (
|
||||
"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页面解释器和配置的字体、几何后端生成独立绘制数据
|
||||
// 入参: r 渲染器, page 页面内容
|
||||
// 返回: *RasterPage 绘制页面, error 编译错误
|
||||
@@ -48,13 +61,6 @@ func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, e
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象
|
||||
type canvasPageCompiler struct {
|
||||
page *RasterPage
|
||||
geometry GeometryBackend
|
||||
err error
|
||||
}
|
||||
|
||||
// Size 返回物理页面尺寸
|
||||
// 返回: float64 宽度, float64 高度
|
||||
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 转换路径和画刷,未知类型返回错误而非丢弃内容
|
||||
// 入参: path 路径, paint 画刷, rule 填充规则, m 变换矩阵
|
||||
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 默认引擎坐标点
|
||||
// 返回: RasterPoint 独立坐标点
|
||||
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)}
|
||||
}
|
||||
@@ -21,6 +21,13 @@ import (
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间
|
||||
type canvasFontMetrics struct {
|
||||
*font.SFNT
|
||||
*fontShaper
|
||||
outlines *sfntOutliner
|
||||
}
|
||||
|
||||
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
|
||||
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
|
||||
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
|
||||
@@ -41,13 +48,6 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间
|
||||
type canvasFontMetrics struct {
|
||||
*font.SFNT
|
||||
*fontShaper
|
||||
outlines *sfntOutliner
|
||||
}
|
||||
|
||||
// Write 返回字体数据,不修改字体元信息
|
||||
// 返回: []byte 独立SFNT数据
|
||||
func (f canvasFontMetrics) Write() []byte {
|
||||
|
||||
@@ -22,14 +22,6 @@ import (
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// canvasNativeStroke 判断描边是否由Canvas提供
|
||||
// 入参: geometry 几何后端
|
||||
// 返回: bool 是否采用原生描边
|
||||
func canvasNativeStroke(geometry GeometryBackend) bool {
|
||||
_, ok := geometry.(CanvasBackend)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Curves 在输出边界转换椭圆弧,其他曲线保持原样
|
||||
// 入参: path 页面路径
|
||||
// 返回: GeometryPath 贝塞尔路径, error 路径错误
|
||||
@@ -41,65 +33,6 @@ func (CanvasBackend) Curves(path GeometryPath) (GeometryPath, error) {
|
||||
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 解析对象的标准紧缩路径并转换到页面坐标
|
||||
// 入参: object 路径对象
|
||||
// 返回: GeometryPath 页面路径, error 无效路径错误
|
||||
@@ -245,6 +178,73 @@ func (CanvasBackend) Clip(r *Renderer, clips *Clips, matrix Matrix, parent *Geom
|
||||
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 将页面坐标路径转换为默认几何引擎的向上纵轴
|
||||
// 入参: path 独立几何路径,nil表示不裁剪
|
||||
// 返回: *canvas.Path 默认引擎路径, error 不支持的路径指令
|
||||
|
||||
@@ -21,6 +21,13 @@ import (
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓
|
||||
type boundsRenderer struct {
|
||||
box Box
|
||||
collect bool
|
||||
contours []ObjectContour
|
||||
}
|
||||
|
||||
// MeasureObject 使用默认解释器度量对象,不分配页面像素
|
||||
// 入参: r 渲染器, object 图形对象, options 度量上下文
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// 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 返回不限定边界的度量画布尺寸
|
||||
// 返回: float64 宽度, float64 高度
|
||||
func (r *boundsRenderer) Size() (float64, float64) {
|
||||
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 收集填充与描边范围,保持线帽、连接和虚线语义
|
||||
// 入参: path 路径, style 绘制样式, m 变换矩阵
|
||||
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
|
||||
}
|
||||
|
||||
// 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 将字形轮廓转换为页面区域
|
||||
// 入参: path 字形轮廓, pageH 页面高度
|
||||
// 返回: Box 字符区域
|
||||
|
||||
@@ -26,6 +26,11 @@ import (
|
||||
"github.com/tdewolff/canvas/renderers/svg"
|
||||
)
|
||||
|
||||
// canvasEPSWriter 在EPS头部写入本库制作软件,不改动绘图内容
|
||||
type canvasEPSWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// RenderSVG 使用canvas输出SVG,按模式保留资源和对象分组
|
||||
// 入参: r 渲染器, page 页面内容, writer 输出流, mode 资源模式
|
||||
// 返回: SVGResources 外部资源, error 错误信息
|
||||
@@ -74,11 +79,6 @@ func (CanvasBackend) RenderEPS(r *Renderer, page *PageContent, writer io.Writer)
|
||||
return buffer.Flush()
|
||||
}
|
||||
|
||||
// canvasEPSWriter 在EPS头部写入本库制作软件,不改动绘图内容
|
||||
type canvasEPSWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// Write 写入EPS片段并替换独立的制作软件声明
|
||||
// 入参: data EPS片段
|
||||
// 返回: int 已处理字节数, error 写入错误
|
||||
|
||||
@@ -27,21 +27,6 @@ type pdfRenderer struct {
|
||||
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导航信息
|
||||
type pdfNavigation struct {
|
||||
Anchor map[int][]pdfAnchor
|
||||
@@ -69,6 +54,21 @@ type pdfOutline struct {
|
||||
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导航信息
|
||||
// 入参: renderer 渲染器, doc 文档结构, pages 页面数据
|
||||
// 返回: *pdfNavigation PDF导航信息, error 几何错误
|
||||
|
||||
@@ -23,6 +23,37 @@ import (
|
||||
"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对象
|
||||
// 入参: ctx 绘制上下文, path 已裁剪轮廓, view 渐变变换, gradient 渐变采样器
|
||||
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))
|
||||
}
|
||||
|
||||
// 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 创建渐变画刷
|
||||
// 入参: gradient 渐变对象, extend 延伸方向, mapType 绘制方式, mapUnit 渐变区间长度
|
||||
// 返回: *shdPaint 渐变画刷
|
||||
|
||||
@@ -55,6 +55,70 @@ type svgObjectGroup struct {
|
||||
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并收集外部资源
|
||||
// 入参: page 页面内容, writer 输出流, images 是否分离图片, objects 是否标识页面直接对象
|
||||
// 返回: SVGResources 外部资源, error 错误信息
|
||||
@@ -149,67 +213,3 @@ func (s *svgResourceRenderer) skipObjects(count int) {
|
||||
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
@@ -31,33 +31,6 @@ type textPageTokens struct {
|
||||
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 页面文字,按绘制顺序保留文本对象,不包含图像和签名外观
|
||||
type PageText struct {
|
||||
Runs []TextRun `json:"runs"`
|
||||
@@ -92,6 +65,33 @@ type TextMatch struct {
|
||||
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 提取页面及启用注释的原文,复用当前编译器的字体和几何语义
|
||||
// 入参: page 页面内容
|
||||
// 返回: *PageText 页面文字, error 错误信息
|
||||
@@ -135,6 +135,62 @@ func (p *PageText) WriteTo(writer io.Writer) (int64, error) {
|
||||
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 输出非空文本对象,可在首个对象前分隔上一页
|
||||
// 入参: writer 输出流, separator 是否需要前导换行
|
||||
// 返回: int64 已写字节数, error 写入错误
|
||||
@@ -167,28 +223,6 @@ func (p *PageText) writeTo(writer io.Writer, separator bool) (int64, error) {
|
||||
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 使用预折叠的查询执行搜索,供跨页搜索复用
|
||||
// 入参: ctx 取消上下文, query 已折叠查询, visit 匹配访问函数
|
||||
// 返回: error 取消或访问错误
|
||||
@@ -240,40 +274,6 @@ func (p *PageText) searchEach(ctx context.Context, query string, visit func(Text
|
||||
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大小写等价字符,保留字符数量
|
||||
// 入参: text 原文
|
||||
// 返回: string 匹配用文字
|
||||
|
||||
+30
-30
@@ -21,16 +21,6 @@ import (
|
||||
"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 (
|
||||
ShapeLine ShapeKind = "line"
|
||||
ShapeArrow ShapeKind = "arrow"
|
||||
@@ -39,6 +29,16 @@ const (
|
||||
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 创建使用标准紧缩路径的图形,默认黑色描边、不填充
|
||||
// 可调整返回对象的颜色、线宽等属性后通过Editor.AddObject写入
|
||||
// 水平、垂直直线使用正尺寸边界,路径端点不变
|
||||
@@ -222,26 +222,6 @@ func (p PathObject) ReshapeFrame(box Box) (PathObject, error) {
|
||||
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 调整基本路径在所在坐标系中的几何范围,保留对象标识、方向、缩放和绘制属性,不缩放线宽
|
||||
// 返回对象可通过Editor.UpdateObject写入;仅支持Shape可识别的路径
|
||||
// 入参: box 新几何范围,直线使用起点和有符号的端点位移
|
||||
@@ -262,6 +242,26 @@ func (p PathObject) ReshapeLine(kind ShapeKind, box Box) (PathObject, error) {
|
||||
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 判断基本图形是否使用直线端点
|
||||
// 入参: kind 图形类型
|
||||
// 返回: bool 是否为直线或箭头
|
||||
|
||||
+11
-11
@@ -23,9 +23,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SignType 签名类型
|
||||
type SignType string
|
||||
|
||||
const (
|
||||
// SignTypeSeal 印章签名
|
||||
SignTypeSeal SignType = "Seal"
|
||||
@@ -33,6 +30,9 @@ const (
|
||||
SignTypeSign SignType = "Sign"
|
||||
)
|
||||
|
||||
// SignType 签名类型
|
||||
type SignType string
|
||||
|
||||
// Signatures 签名列表
|
||||
type Signatures struct {
|
||||
XMLName xml.Name `xml:"Signatures"`
|
||||
@@ -107,6 +107,14 @@ type SignatureReference struct {
|
||||
CheckValue string `xml:"CheckValue"`
|
||||
}
|
||||
|
||||
// Stamp 印章信息结构
|
||||
type Stamp struct {
|
||||
Box Box
|
||||
Clip *Box
|
||||
Type string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// SignatureStampPositions 获取签名外观位置信息
|
||||
// 入参: stamps 签名外观列表
|
||||
// 返回: []SignatureStampPosition 签名外观位置信息, error 错误信息
|
||||
@@ -421,14 +429,6 @@ func probeImageMedia(data []byte) (string, []byte) {
|
||||
return normalizeSealType(format), data
|
||||
}
|
||||
|
||||
// Stamp 印章信息结构
|
||||
type Stamp struct {
|
||||
Box Box
|
||||
Clip *Box
|
||||
Type string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// addStamp 添加印章到页面
|
||||
// 入参: pageID 页面ID, box 印章区域, clip 裁剪区域, sType 印章类型, data 印章数据
|
||||
func (r *Reader) addStamp(pageID string, box Box, clip *Box, sType string, data []byte) {
|
||||
|
||||
+21
-21
@@ -46,10 +46,31 @@ type SignaturePolicy struct {
|
||||
// signaturePolicyRejection 区分策略拒绝与解析失败
|
||||
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 返回策略拒绝原因
|
||||
// 返回: string 错误信息
|
||||
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 构造可识别的策略拒绝错误
|
||||
// 入参: format 消息模板, values 模板参数
|
||||
// 返回: 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 应用版本和算法限制
|
||||
// 入参: o 验签选项, ofdVersion OFD版本, sesVersion SES版本, method 签名算法, digest 摘要算法
|
||||
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
|
||||
}
|
||||
|
||||
// signatureCoverageNode 收集OFD结构中的包内路径引用
|
||||
type signatureCoverageNode struct {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:",any,attr"`
|
||||
Text string `xml:",chardata"`
|
||||
Children []signatureCoverageNode `xml:",any"`
|
||||
}
|
||||
|
||||
// signatureCoverageReferences 收集标准OFD引用的传递闭包且包含跨文档资源
|
||||
// 入参: root 文档入口, required 待补充的必需文件集合
|
||||
// 返回: error 引用文件无法读取或解析时的错误
|
||||
|
||||
+31
-31
@@ -45,37 +45,6 @@ type ParsedSignatureSeal struct {
|
||||
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签章值
|
||||
type sesSignature struct {
|
||||
Timestamp []byte
|
||||
@@ -137,6 +106,37 @@ type sesVerifyResult struct {
|
||||
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签章值
|
||||
// 入参: data 签章值数据
|
||||
// 返回: *sesSignature SES签章值, error 错误信息
|
||||
|
||||
+11
-11
@@ -99,17 +99,6 @@ func WithSignatureTimestamp(options SignatureTimestampOptions) SignatureVerifyOp
|
||||
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令牌与原始字节的绑定
|
||||
// 入参: token DER编码TimeStampToken, data 被时间戳保护的原始字节, options 验证选项
|
||||
// 返回: SignatureTimestampReport 验证报告
|
||||
@@ -122,6 +111,17 @@ func VerifySignatureTimestamp(token, data []byte, options SignatureTimestampOpti
|
||||
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信任
|
||||
// 入参: token 令牌DER, data 原始字节, o 验证选项, report 结果
|
||||
// 返回: error 验证错误
|
||||
|
||||
+42
-42
@@ -39,6 +39,8 @@ import (
|
||||
"github.com/emmansun/gmsm/smx509"
|
||||
)
|
||||
|
||||
var signatureMethodReplacer = strings.NewReplacer("-", "", "_", "", " ", "")
|
||||
|
||||
// SignatureVerifyReport 签名验证报告
|
||||
// Valid表示签名完整性、签名时间语义及调用方指定的全部策略均通过
|
||||
// Checked表示对应检查已得出结论, 未检查时不能将OK的零值视为失败
|
||||
@@ -103,36 +105,6 @@ type SignatureVerifyReport struct {
|
||||
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 签名证书信息
|
||||
type SignatureCertInfo struct {
|
||||
Raw []byte
|
||||
@@ -180,11 +152,39 @@ type signatureVerifyOptions struct {
|
||||
DocRoot string
|
||||
}
|
||||
|
||||
var signatureMethodReplacer = strings.NewReplacer("-", "", "_", "", " ", "")
|
||||
|
||||
// SignatureVerifyOption 签名验证选项函数
|
||||
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 添加数字签名验证证书
|
||||
// 入参: cert DER或PEM编码证书
|
||||
// 返回: 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字节数据签名
|
||||
// 入参: data OFD字节数据, opts 签名验证选项
|
||||
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
|
||||
@@ -342,6 +332,16 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti
|
||||
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 验证单个签名
|
||||
// 入参: sigListPath 签名列表路径, sigRef 签名引用, options 验证选项
|
||||
// 返回: SignatureVerifyReport 签名验证报告
|
||||
|
||||
+13
-13
@@ -29,9 +29,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// SignatureWriteMode 签署方式
|
||||
type SignatureWriteMode uint8
|
||||
|
||||
const (
|
||||
// SignatureAppend 追加签署,要求原活动签名有效且未保护签名列表
|
||||
SignatureAppend SignatureWriteMode = iota
|
||||
@@ -39,6 +36,9 @@ const (
|
||||
SignatureReplace
|
||||
)
|
||||
|
||||
// SignatureWriteMode 签署方式
|
||||
type SignatureWriteMode uint8
|
||||
|
||||
// SignatureWriteOptions 最终包签署选项,不修改Editor或调用方的原件
|
||||
// SM2数字签名使用GB/T 35275-2017消息结构;RSA/ECDSA使用裸签名,验签需WithSignatureCert
|
||||
// Seal仅接受已有合法SES v4电子印章,使用SM2/SM3,不声明GM/T 0031-2025兼容
|
||||
@@ -64,6 +64,16 @@ type SignatureWriteOptions struct {
|
||||
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且不修改输入
|
||||
// 入参: data 最终包字节, options 签署选项
|
||||
// 返回: []byte 签署后的完整包, error 错误信息
|
||||
@@ -436,16 +446,6 @@ func signatureWriteIDs(list *Signatures, listPath string, parts map[string][]byt
|
||||
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命名空间的签名文件
|
||||
// 入参: name 根节点名称, value 内容
|
||||
// 返回: []byte XML, error 错误信息
|
||||
|
||||
@@ -37,6 +37,13 @@ import (
|
||||
"github.com/emmansun/gmsm/smx509"
|
||||
)
|
||||
|
||||
// signatureWriteKey 已验证签署身份
|
||||
type signatureWriteKey struct {
|
||||
cert *smx509.Certificate
|
||||
method string
|
||||
digest string
|
||||
}
|
||||
|
||||
// ParseSignatureIdentity 解析签署私钥和证书,并检查公私钥对应关系
|
||||
// 支持普通PKCS8、SEC1、PKCS1以及加密PKCS8,不支持传统加密PEM
|
||||
// 不加载系统信任或暗中信任该证书,返回的SM2/RSA私钥同时实现crypto.Decrypter
|
||||
@@ -97,13 +104,6 @@ func ParseSignatureIdentity(certificate, privateKey []byte, password ...[]byte)
|
||||
return signer, bytes.Clone(cert.Raw), nil
|
||||
}
|
||||
|
||||
// signatureWriteKey 已验证签署身份
|
||||
type signatureWriteKey struct {
|
||||
cert *smx509.Certificate
|
||||
method string
|
||||
digest string
|
||||
}
|
||||
|
||||
// signatureWriteIdentity 检查签署身份、证书信任与印章授权
|
||||
// 制章证书按制章时间验证,不以签署时间代替制章时间
|
||||
// 入参: options 签署选项
|
||||
|
||||
+24
-24
@@ -21,6 +21,18 @@ import (
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// textGlyph 绘制字形
|
||||
type textGlyph struct {
|
||||
Text string
|
||||
GlyphID int
|
||||
}
|
||||
|
||||
// textGlyphTransform 字符到字形变换
|
||||
type textGlyphTransform struct {
|
||||
CodeCount int
|
||||
Glyphs []textGlyph
|
||||
}
|
||||
|
||||
// Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示
|
||||
// 返回: string 原文
|
||||
func (obj TextObject) Text() string {
|
||||
@@ -38,6 +50,18 @@ func (obj TextObject) 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 判断横向文字的行首回退,或有足够行距且水平范围重叠的对齐段落
|
||||
// 入参: index 文本编码索引
|
||||
// 返回: bool 是否换行
|
||||
@@ -67,18 +91,6 @@ func (obj TextObject) textCodeLineBreak(index int) bool {
|
||||
return x <= right
|
||||
}
|
||||
|
||||
// textGlyph 绘制字形
|
||||
type textGlyph struct {
|
||||
Text string
|
||||
GlyphID int
|
||||
}
|
||||
|
||||
// textGlyphTransform 字符到字形变换
|
||||
type textGlyphTransform struct {
|
||||
CodeCount int
|
||||
Glyphs []textGlyph
|
||||
}
|
||||
|
||||
// textObjectFontID 获取文本对象字体ID
|
||||
// 入参: text 文本对象
|
||||
// 返回: string 字体ID
|
||||
@@ -305,15 +317,3 @@ func textCodeRunes(value string) []rune {
|
||||
}
|
||||
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
@@ -29,6 +29,13 @@ type configuredTextShaper struct {
|
||||
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 将显式塑形结果保存为标准文字定位,成功前不修改对象
|
||||
// 入参: obj 文字对象, value 原文, options 排版选项, metrics 字体度量, width 行宽, lineHeight 行高, hScale 横向比例, baseline 首行基线
|
||||
// 返回: error 塑形或排版错误
|
||||
@@ -198,13 +205,6 @@ func bidiClusterShifts(glyphs []ShapedGlyph, extra []float64) map[int]float64 {
|
||||
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 检查可选后端的簇映射,按原文字符分配塑形步进
|
||||
// 入参: shaper 字体塑形器, runes 单行字符, font 字体标识, size 字号, scale 横向比例, spacing 字距
|
||||
// 返回: []ShapedGlyph 字形, []float64 含字距的字符步进, error 塑形错误
|
||||
|
||||
+75
-75
@@ -35,16 +35,21 @@ const ofdNamespace = "http://www.ofdspec.org/2016"
|
||||
// editorProgress 保存准备阶段的进度检查点,内存预览使用nil
|
||||
type editorProgress func(stage string, completed, total int) error
|
||||
|
||||
// report 回报进度并原样传递调用方停止原因
|
||||
// 入参: stage 阶段, completed 已处理项, total 总项数
|
||||
// 返回: error 调用方停止原因
|
||||
func (progress editorProgress) report(stage string, completed, total int) error {
|
||||
if progress != nil {
|
||||
return progress(stage, completed, total)
|
||||
// ofdCountingWriter 记录实际输出字节数
|
||||
type ofdCountingWriter struct {
|
||||
writer io.Writer
|
||||
count int64
|
||||
}
|
||||
return nil
|
||||
|
||||
// ofdXML 使用标准ofd命名空间写出XML
|
||||
type ofdXML struct {
|
||||
encoder *xml.Encoder
|
||||
err error
|
||||
}
|
||||
|
||||
// ofdAttrs OFD节点属性
|
||||
type ofdAttrs []xml.Attr
|
||||
|
||||
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
||||
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
|
||||
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
|
||||
@@ -59,47 +64,6 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
||||
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,不修改当前文档或撤销记录
|
||||
// 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转
|
||||
// 快照只准备所选页面,关联引用由页面导入流程统一筛选
|
||||
@@ -160,6 +124,69 @@ func (e *Editor) Reader() (*Reader, error) {
|
||||
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 生成内存快照,保存时复用准备进度,预览时不触发回调
|
||||
// 入参: progress 准备进度回调
|
||||
// 返回: *Reader 阅读器, error 错误信息
|
||||
@@ -525,30 +552,6 @@ func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace)
|
||||
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编码器
|
||||
// 入参: writer 输出流
|
||||
// 返回: *ofdXML 编码器
|
||||
@@ -601,9 +604,6 @@ func (x *ofdXML) finish() error {
|
||||
return x.encoder.Close()
|
||||
}
|
||||
|
||||
// ofdAttrs OFD节点属性
|
||||
type ofdAttrs []xml.Attr
|
||||
|
||||
// add 添加非空属性
|
||||
// 入参: name 属性名, value 属性值
|
||||
func (a *ofdAttrs) add(name, value string) {
|
||||
|
||||
Reference in new issue
Block a user