From ee6c9f6a1618f4ca210e165097a7ea4421d699e3 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 28 Sep 2026 15:17:45 +0800 Subject: [PATCH] =?UTF-8?q?style(=E4=BB=A3=E7=A0=81=E9=A3=8E=E6=A0=BC):=20?= =?UTF-8?q?=E6=95=B4=E7=90=86=E5=A3=B0=E6=98=8E=E9=A1=BA=E5=BA=8F=E4=B8=8E?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- internal/webui/wasm.go | 70 +-- internal/webui/webui.go | 324 +++++++------- ofdgo_action.go | 30 +- ofdgo_backend.go | 92 ++-- ofdgo_crypto.go | 42 +- ofdgo_crypto_package.go | 34 +- ofdgo_doc.go | 58 +-- ofdgo_editor.go | 382 ++++++++-------- ofdgo_editor_batch.go | 386 ++++++++-------- ofdgo_editor_composite.go | 274 ++++++------ ofdgo_editor_composite_copy.go | 280 ++++++------ ofdgo_editor_composite_image.go | 218 +++++----- ofdgo_editor_composite_ops.go | 144 +++--- ofdgo_editor_erase.go | 46 +- ofdgo_editor_error.go | 6 +- ofdgo_editor_history.go | 94 ++-- ofdgo_editor_import.go | 40 +- ofdgo_editor_link.go | 200 ++++----- ofdgo_editor_open.go | 108 ++--- ofdgo_editor_style.go | 330 +++++++------- ofdgo_editor_text.go | 88 ++-- ofdgo_editor_transform.go | 200 ++++----- ofdgo_font.go | 16 +- ofdgo_font_backend.go | 46 +- ofdgo_font_cff.go | 30 +- ofdgo_font_cmap.go | 391 +++++++++++++++++ ofdgo_font_file.go | 40 +- ofdgo_font_fs.go | 371 ++++++++-------- ofdgo_font_info.go | 152 +++---- ofdgo_font_match.go | 50 +-- ofdgo_font_shape.go | 6 +- ofdgo_font_source.go | 18 +- ofdgo_font_type1.go | 80 ++-- ofdgo_font_util.go | 410 +---------------- ofdgo_geom.go | 54 +-- ofdgo_geom_backend.go | 48 +- ofdgo_geom_path.go | 38 +- ofdgo_geom_transform.go | 70 +-- ofdgo_image.go | 202 ++++----- ofdgo_image_resource.go | 28 +- ofdgo_object.go | 136 +++--- ofdgo_paint.go | 40 +- ofdgo_paint_mesh.go | 62 +-- ofdgo_paint_model.go | 18 +- ofdgo_pdf.go | 26 +- ofdgo_pdf_mesh.go | 60 +-- ofdgo_raster.go | 38 +- ofdgo_raster_image.go | 24 +- ofdgo_raster_scene.go | 12 +- ofdgo_reader.go | 724 +++++++++++++++---------------- ofdgo_render_bounds.go | 12 +- ofdgo_render_compile.go | 30 +- ofdgo_render_compile_image.go | 14 +- ofdgo_render_output.go | 118 ++--- ofdgo_render_pattern.go | 18 +- ofdgo_render_scene.go | 42 +- ofdgo_render_text.go | 308 ++++++------- ofdgo_render_walk.go | 14 +- ofdgo_renderer_canvas.go | 44 +- ofdgo_renderer_clip_canvas.go | 20 +- ofdgo_renderer_compile_canvas.go | 130 +++--- ofdgo_renderer_font_canvas.go | 14 +- ofdgo_renderer_geom_canvas.go | 134 +++--- ofdgo_renderer_measure_canvas.go | 144 +++--- ofdgo_renderer_output_canvas.go | 10 +- ofdgo_renderer_pdf_canvas.go | 30 +- ofdgo_renderer_shading_canvas.go | 62 +-- ofdgo_renderer_svg_canvas.go | 128 +++--- ofdgo_search.go | 166 +++---- ofdgo_shape.go | 60 +-- ofdgo_sign.go | 22 +- ofdgo_sign_policy.go | 42 +- ofdgo_sign_ses.go | 62 +-- ofdgo_sign_timestamp.go | 22 +- ofdgo_sign_verify.go | 84 ++-- ofdgo_sign_write.go | 26 +- ofdgo_sign_write_crypto.go | 14 +- ofdgo_text.go | 48 +- ofdgo_text_shape.go | 14 +- ofdgo_writer.go | 152 +++---- 82 files changed, 4316 insertions(+), 4310 deletions(-) diff --git a/go.mod b/go.mod index 5c9d2b5..27c7e02 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 8255429..f227247 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 5608e48..d895340 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -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)) { diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 3530ccd..9573019 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -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 { diff --git a/ofdgo_action.go b/ofdgo_action.go index 3f9d604..5992ddc 100644 --- a/ofdgo_action.go +++ b/ofdgo_action.go @@ -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 属性值 diff --git a/ofdgo_backend.go b/ofdgo_backend.go index 873413c..4abd2a8 100644 --- a/ofdgo_backend.go +++ b/ofdgo_backend.go @@ -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) -} diff --git a/ofdgo_crypto.go b/ofdgo_crypto.go index 8e04b3c..8f9068e 100644 --- a/ofdgo_crypto.go +++ b/ofdgo_crypto.go @@ -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 { diff --git a/ofdgo_crypto_package.go b/ofdgo_crypto_package.go index e2d630f..c32216a 100644 --- a/ofdgo_crypto_package.go +++ b/ofdgo_crypto_package.go @@ -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 错误信息 diff --git a/ofdgo_doc.go b/ofdgo_doc.go index e5d4442..820495d 100644 --- a/ofdgo_doc.go +++ b/ofdgo_doc.go @@ -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 获取默认文档权限 diff --git a/ofdgo_editor.go b/ofdgo_editor.go index 0e7cf79..75be0c4 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -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 错误信息 diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index 110e306..264c951 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -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 - } - return cmp.Compare(a.index, b.index) +// ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象 +type ObjectPosition struct { + Layer string + Container string + Index int + Count int +} + +// 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 错误信息 diff --git a/ofdgo_editor_composite.go b/ofdgo_editor_composite.go index 5006b2d..d1a0d4f 100644 --- a/ofdgo_editor_composite.go +++ b/ofdgo_editor_composite.go @@ -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 错误信息 diff --git a/ofdgo_editor_composite_copy.go b/ofdgo_editor_composite_copy.go index 89ad615..c26c9ab 100644 --- a/ofdgo_editor_composite_copy.go +++ b/ofdgo_editor_composite_copy.go @@ -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 范围节点 diff --git a/ofdgo_editor_composite_image.go b/ofdgo_editor_composite_image.go index f6b8317..8f02193 100644 --- a/ofdgo_editor_composite_image.go +++ b/ofdgo_editor_composite_image.go @@ -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 +} diff --git a/ofdgo_editor_composite_ops.go b/ofdgo_editor_composite_ops.go index 86bb598..ef8bc71 100644 --- a/ofdgo_editor_composite_ops.go +++ b/ofdgo_editor_composite_ops.go @@ -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 错误信息 diff --git a/ofdgo_editor_erase.go b/ofdgo_editor_erase.go index 7e741d6..06047eb 100644 --- a/ofdgo_editor_erase.go +++ b/ofdgo_editor_erase.go @@ -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 错误信息 diff --git a/ofdgo_editor_error.go b/ofdgo_editor_error.go index a4d152e..3152d5d 100644 --- a/ofdgo_editor_error.go +++ b/ofdgo_editor_error.go @@ -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 diff --git a/ofdgo_editor_history.go b/ofdgo_editor_history.go index 66be880..391ba21 100644 --- a/ofdgo_editor_history.go +++ b/ofdgo_editor_history.go @@ -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 { diff --git a/ofdgo_editor_import.go b/ofdgo_editor_import.go index 0c77914..f5938ff 100644 --- a/ofdgo_editor_import.go +++ b/ofdgo_editor_import.go @@ -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) { diff --git a/ofdgo_editor_link.go b/ofdgo_editor_link.go index 2ffc099..50486e0 100644 --- a/ofdgo_editor_link.go +++ b/ofdgo_editor_link.go @@ -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 错误信息 diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 5cbdc2f..619960d 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -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 是否可准确度量 diff --git a/ofdgo_editor_style.go b/ofdgo_editor_style.go index 43e1fad..d7d696a 100644 --- a/ofdgo_editor_style.go +++ b/ofdgo_editor_style.go @@ -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 错误信息 diff --git a/ofdgo_editor_text.go b/ofdgo_editor_text.go index c6d1741..9613180 100644 --- a/ofdgo_editor_text.go +++ b/ofdgo_editor_text.go @@ -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 各行的字符起止索引,左闭右开 diff --git a/ofdgo_editor_transform.go b/ofdgo_editor_transform.go index 5d844c8..5daa957 100644 --- a/ofdgo_editor_transform.go +++ b/ofdgo_editor_transform.go @@ -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属性值 diff --git a/ofdgo_font.go b/ofdgo_font.go index 4974978..ef4f4c8 100644 --- a/ofdgo_font.go +++ b/ofdgo_font.go @@ -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裸数据 diff --git a/ofdgo_font_backend.go b/ofdgo_font_backend.go index 8e9fd94..d612217 100644 --- a/ofdgo_font_backend.go +++ b/ofdgo_font_backend.go @@ -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() + } + } +} diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go index 0206ee8..e948c4b 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -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", -} diff --git a/ofdgo_font_cmap.go b/ofdgo_font_cmap.go index 1ad010d..9032589 100644 --- a/ofdgo_font_cmap.go +++ b/ofdgo_font_cmap.go @@ -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() +} diff --git a/ofdgo_font_file.go b/ofdgo_font_file.go index dd59835..ea4551e 100644 --- a/ofdgo_font_file.go +++ b/ofdgo_font_file.go @@ -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 格式错误 diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index 758c286..b585bd6 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -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) diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index 8a77dca..2db0d38 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -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 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 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 错误信息 diff --git a/ofdgo_font_match.go b/ofdgo_font_match.go index 85f7c49..9ef7960 100644 --- a/ofdgo_font_match.go +++ b/ofdgo_font_match.go @@ -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 规范化后的字体名称 diff --git a/ofdgo_font_shape.go b/ofdgo_font_shape.go index 9bdbb99..a170f3a 100644 --- a/ofdgo_font_shape.go +++ b/ofdgo_font_shape.go @@ -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 字体 diff --git a/ofdgo_font_source.go b/ofdgo_font_source.go index c6eca06..94e9b56 100644 --- a/ofdgo_font_source.go +++ b/ofdgo_font_source.go @@ -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 读取或集合错误 diff --git a/ofdgo_font_type1.go b/ofdgo_font_type1.go index dfa5772..906d4d1 100644 --- a/ofdgo_font_type1.go +++ b/ofdgo_font_type1.go @@ -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 { diff --git a/ofdgo_font_util.go b/ofdgo_font_util.go index 5694924..88467b3 100644 --- a/ofdgo_font_util.go +++ b/ofdgo_font_util.go @@ -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 错误信息 diff --git a/ofdgo_geom.go b/ofdgo_geom.go index c36b380..aff4224 100644 --- a/ofdgo_geom.go +++ b/ofdgo_geom.go @@ -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 浮点数数组 diff --git a/ofdgo_geom_backend.go b/ofdgo_geom_backend.go index c4c1f7b..d061589 100644 --- a/ofdgo_geom_backend.go +++ b/ofdgo_geom_backend.go @@ -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 闭合路径 diff --git a/ofdgo_geom_path.go b/ofdgo_geom_path.go index 2cb96c7..575f695 100644 --- a/ofdgo_geom_path.go +++ b/ofdgo_geom_path.go @@ -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 不支持的路径指令 diff --git a/ofdgo_geom_transform.go b/ofdgo_geom_transform.go index 773e5e3..7ad17ed 100644 --- a/ofdgo_geom_transform.go +++ b/ofdgo_geom_transform.go @@ -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 +} diff --git a/ofdgo_image.go b/ofdgo_image.go index 19f874e..d751400 100644 --- a/ofdgo_image.go +++ b/ofdgo_image.go @@ -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)} -} diff --git a/ofdgo_image_resource.go b/ofdgo_image_resource.go index 4bdd997..04bd5fc 100644 --- a/ofdgo_image_resource.go +++ b/ofdgo_image_resource.go @@ -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 读取错误 diff --git a/ofdgo_object.go b/ofdgo_object.go index c139267..fc2ff22 100644 --- a/ofdgo_object.go +++ b/ofdgo_object.go @@ -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 属性值 diff --git a/ofdgo_paint.go b/ofdgo_paint.go index e18f91b..3b597e7 100644 --- a/ofdgo_paint.go +++ b/ofdgo_paint.go @@ -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 仅供绘制的颜色节点,不改变原始分段 diff --git a/ofdgo_paint_mesh.go b/ofdgo_paint_mesh.go index 35427c7..c6fa310 100644 --- a/ofdgo_paint_mesh.go +++ b/ofdgo_paint_mesh.go @@ -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 红、绿、蓝分量 diff --git a/ofdgo_paint_model.go b/ofdgo_paint_model.go index 78e070b..f2c705f 100644 --- a/ofdgo_paint_model.go +++ b/ofdgo_paint_model.go @@ -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 diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index fcead51..a4e3d17 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -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 页面高度 diff --git a/ofdgo_pdf_mesh.go b/ofdgo_pdf_mesh.go index 1c28093..06d681b 100644 --- a/ofdgo_pdf_mesh.go +++ b/ofdgo_pdf_mesh.go @@ -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 取消或坐标错误 diff --git a/ofdgo_raster.go b/ofdgo_raster.go index df980b7..76b0dc3 100644 --- a/ofdgo_raster.go +++ b/ofdgo_raster.go @@ -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 是否覆盖采样点 diff --git a/ofdgo_raster_image.go b/ofdgo_raster_image.go index e965fd5..f0b8e01 100644 --- a/ofdgo_raster_image.go +++ b/ofdgo_raster_image.go @@ -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 -} diff --git a/ofdgo_raster_scene.go b/ofdgo_raster_scene.go index f687245..9dc4d17 100644 --- a/ofdgo_raster_scene.go +++ b/ofdgo_raster_scene.go @@ -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) } diff --git a/ofdgo_reader.go b/ofdgo_reader.go index 37a0354..6eb0957 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -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 +} diff --git a/ofdgo_render_bounds.go b/ofdgo_render_bounds.go index bbd435e..d7622c7 100644 --- a/ofdgo_render_bounds.go +++ b/ofdgo_render_bounds.go @@ -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 注解 diff --git a/ofdgo_render_compile.go b/ofdgo_render_compile.go index bf86502..73f3c10 100644 --- a/ofdgo_render_compile.go +++ b/ofdgo_render_compile.go @@ -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 解析样式 diff --git a/ofdgo_render_compile_image.go b/ofdgo_render_compile_image.go index 4e911f9..30d1db7 100644 --- a/ofdgo_render_compile_image.go +++ b/ofdgo_render_compile_image.go @@ -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 印章解析或编译错误 diff --git a/ofdgo_render_output.go b/ofdgo_render_output.go index 21e8d0a..9b5ba02 100644 --- a/ofdgo_render_output.go +++ b/ofdgo_render_output.go @@ -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 错误信息 diff --git a/ofdgo_render_pattern.go b/ofdgo_render_pattern.go index d804977..dc4dae7 100644 --- a/ofdgo_render_pattern.go +++ b/ofdgo_render_pattern.go @@ -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 编译错误 diff --git a/ofdgo_render_scene.go b/ofdgo_render_scene.go index 7a4e5a3..3c48053 100644 --- a/ofdgo_render_scene.go +++ b/ofdgo_render_scene.go @@ -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() + } + } +} diff --git a/ofdgo_render_text.go b/ofdgo_render_text.go index 8817da6..b413490 100644 --- a/ofdgo_render_text.go +++ b/ofdgo_render_text.go @@ -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 几何错误 diff --git a/ofdgo_render_walk.go b/ofdgo_render_walk.go index 76025cd..27e0a7e 100644 --- a/ofdgo_render_walk.go +++ b/ofdgo_render_walk.go @@ -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 遍历或访问器错误 diff --git a/ofdgo_renderer_canvas.go b/ofdgo_renderer_canvas.go index faca27d..29b4a1b 100644 --- a/ofdgo_renderer_canvas.go +++ b/ofdgo_renderer_canvas.go @@ -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} +} diff --git a/ofdgo_renderer_clip_canvas.go b/ofdgo_renderer_clip_canvas.go index 85716ac..36a9c02 100644 --- a/ofdgo_renderer_clip_canvas.go +++ b/ofdgo_renderer_clip_canvas.go @@ -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 路径对象 diff --git a/ofdgo_renderer_compile_canvas.go b/ofdgo_renderer_compile_canvas.go index c0e4667..f84454c 100644 --- a/ofdgo_renderer_compile_canvas.go +++ b/ofdgo_renderer_compile_canvas.go @@ -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)} -} diff --git a/ofdgo_renderer_font_canvas.go b/ofdgo_renderer_font_canvas.go index e4d1858..583bd6c 100644 --- a/ofdgo_renderer_font_canvas.go +++ b/ofdgo_renderer_font_canvas.go @@ -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 { diff --git a/ofdgo_renderer_geom_canvas.go b/ofdgo_renderer_geom_canvas.go index cad605f..60e944c 100644 --- a/ofdgo_renderer_geom_canvas.go +++ b/ofdgo_renderer_geom_canvas.go @@ -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 不支持的路径指令 diff --git a/ofdgo_renderer_measure_canvas.go b/ofdgo_renderer_measure_canvas.go index 9356c2a..fb76347 100644 --- a/ofdgo_renderer_measure_canvas.go +++ b/ofdgo_renderer_measure_canvas.go @@ -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 字符区域 diff --git a/ofdgo_renderer_output_canvas.go b/ofdgo_renderer_output_canvas.go index c143ff2..152d02a 100644 --- a/ofdgo_renderer_output_canvas.go +++ b/ofdgo_renderer_output_canvas.go @@ -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 写入错误 diff --git a/ofdgo_renderer_pdf_canvas.go b/ofdgo_renderer_pdf_canvas.go index 7175286..8387f8d 100644 --- a/ofdgo_renderer_pdf_canvas.go +++ b/ofdgo_renderer_pdf_canvas.go @@ -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 几何错误 diff --git a/ofdgo_renderer_shading_canvas.go b/ofdgo_renderer_shading_canvas.go index 8af4f58..d2a11fb 100644 --- a/ofdgo_renderer_shading_canvas.go +++ b/ofdgo_renderer_shading_canvas.go @@ -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 渐变画刷 diff --git a/ofdgo_renderer_svg_canvas.go b/ofdgo_renderer_svg_canvas.go index f4e8688..9e2a508 100644 --- a/ofdgo_renderer_svg_canvas.go +++ b/ofdgo_renderer_svg_canvas.go @@ -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, ``, 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, ``, 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() -} diff --git a/ofdgo_search.go b/ofdgo_search.go index 4b666ad..ae28bf8 100644 --- a/ofdgo_search.go +++ b/ofdgo_search.go @@ -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 匹配用文字 diff --git a/ofdgo_shape.go b/ofdgo_shape.go index 894eec0..ace0b45 100644 --- a/ofdgo_shape.go +++ b/ofdgo_shape.go @@ -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 是否为直线或箭头 diff --git a/ofdgo_sign.go b/ofdgo_sign.go index 7f94d4b..77ce315 100644 --- a/ofdgo_sign.go +++ b/ofdgo_sign.go @@ -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) { diff --git a/ofdgo_sign_policy.go b/ofdgo_sign_policy.go index d3aa5db..2ddb446 100644 --- a/ofdgo_sign_policy.go +++ b/ofdgo_sign_policy.go @@ -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 引用文件无法读取或解析时的错误 diff --git a/ofdgo_sign_ses.go b/ofdgo_sign_ses.go index 8679234..3bc3c03 100644 --- a/ofdgo_sign_ses.go +++ b/ofdgo_sign_ses.go @@ -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 错误信息 diff --git a/ofdgo_sign_timestamp.go b/ofdgo_sign_timestamp.go index 7560023..76ed489 100644 --- a/ofdgo_sign_timestamp.go +++ b/ofdgo_sign_timestamp.go @@ -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 验证错误 diff --git a/ofdgo_sign_verify.go b/ofdgo_sign_verify.go index 7a7436b..5357edd 100644 --- a/ofdgo_sign_verify.go +++ b/ofdgo_sign_verify.go @@ -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 签名验证报告 diff --git a/ofdgo_sign_write.go b/ofdgo_sign_write.go index 5584586..e3dea67 100644 --- a/ofdgo_sign_write.go +++ b/ofdgo_sign_write.go @@ -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 错误信息 diff --git a/ofdgo_sign_write_crypto.go b/ofdgo_sign_write_crypto.go index 98c3414..7601bbf 100644 --- a/ofdgo_sign_write_crypto.go +++ b/ofdgo_sign_write_crypto.go @@ -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 签署选项 diff --git a/ofdgo_text.go b/ofdgo_text.go index 037df58..ac2140f 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -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) -} diff --git a/ofdgo_text_shape.go b/ofdgo_text_shape.go index ca8992f..4073cfb 100644 --- a/ofdgo_text_shape.go +++ b/ofdgo_text_shape.go @@ -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 塑形错误 diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 9621ffd..0dd1f26 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -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) - } - return nil +// ofdCountingWriter 记录实际输出字节数 +type ofdCountingWriter struct { + writer io.Writer + count int64 } +// 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) {