From d07972346b0c34c10f9bb816bbfb71b29445e23d Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 14 Oct 2026 09:38:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=87=E4=BB=B6=E5=BD=92=E5=B9=B6):=20?= =?UTF-8?q?=E5=BD=92=E5=B9=B6=E5=90=8C=E7=B1=BB=E6=96=87=E4=BB=B6=E6=B8=85?= =?UTF-8?q?=E7=90=86=E8=BE=85=E5=8A=A9=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_backend.go | 52 +++++++ ofdgo_backend_registry.go | 71 ---------- ofdgo_compression_images.go | 122 ++++++++++++++++ ofdgo_compression_pattern_images.go | 142 ------------------- ofdgo_editor_resource_graph.go | 108 -------------- ofdgo_editor_resource_scan.go | 203 +++++++++++++++++++++++++++ ofdgo_editor_resource_scope.go | 137 ------------------ ofdgo_editor_selection.go | 62 ++++++++ ofdgo_editor_selection_copy.go | 79 ----------- ofdgo_renderer_font_canvas.go | 179 +++++++++++++++++++++++ ofdgo_renderer_font_loader_canvas.go | 78 ---------- ofdgo_renderer_glyph_canvas.go | 142 ------------------- ofdgo_renderer_layer_canvas.go | 119 ---------------- ofdgo_renderer_page_canvas.go | 102 ++++++++++++++ ofdgo_sign_id.go | 43 ------ ofdgo_sign_write.go | 22 +++ ofdgo_structure_object.go | 110 --------------- ofdgo_structure_target.go | 93 ++++++++++++ ofdgo_text.go | 13 -- 19 files changed, 835 insertions(+), 1042 deletions(-) delete mode 100644 ofdgo_backend_registry.go delete mode 100644 ofdgo_compression_pattern_images.go delete mode 100644 ofdgo_editor_resource_graph.go delete mode 100644 ofdgo_editor_resource_scope.go delete mode 100644 ofdgo_editor_selection_copy.go delete mode 100644 ofdgo_renderer_font_loader_canvas.go delete mode 100644 ofdgo_renderer_glyph_canvas.go delete mode 100644 ofdgo_renderer_layer_canvas.go delete mode 100644 ofdgo_sign_id.go delete mode 100644 ofdgo_structure_object.go diff --git a/ofdgo_backend.go b/ofdgo_backend.go index 7733317..e683ea6 100644 --- a/ofdgo_backend.go +++ b/ofdgo_backend.go @@ -21,6 +21,7 @@ import ( "image" "io" "reflect" + "sync" ) // SVG输出模式,控制资源内嵌和对象结构 @@ -34,6 +35,12 @@ const ( // ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现 var ErrBackendUnavailable = errors.New("render backend unavailable") +// backendRegistry 保存后端工厂,不共享有状态的后端实例 +var backendRegistry = struct { + sync.RWMutex + factories map[string]func() RenderBackends +}{factories: make(map[string]func() RenderBackends)} + // Backend 提供稳定的后端标识,不包含前端文案 type Backend interface { Name() string @@ -284,3 +291,48 @@ func immutableBackendValue(value reflect.Value) bool { } return true } + +// RegisterRenderBackend 注册后端组合,工厂应返回独立实例,重复标识不会覆盖已有实现 +// 入参: name 后端标识, factory 后端工厂 +// 返回: error 无效或重复注册错误 +func RegisterRenderBackend(name string, factory func() RenderBackends) error { + if name == "" || factory == nil { + return fmt.Errorf("backend name and factory are required") + } + backendRegistry.Lock() + defer backendRegistry.Unlock() + if _, exists := backendRegistry.factories[name]; exists { + return fmt.Errorf("render backend %q already registered", name) + } + backendRegistry.factories[name] = factory + return nil +} + +// NewRenderBackends 创建已注册后端组合,各项能力通过Info报告实际提供者 +// 入参: name 已注册后端标识 +// 返回: RenderBackends 后端组合, error 未知后端错误 +func NewRenderBackends(name string) (RenderBackends, error) { + backendRegistry.RLock() + factory := backendRegistry.factories[name] + backendRegistry.RUnlock() + if factory == nil { + return RenderBackends{}, fmt.Errorf("unknown render backend %q", name) + } + return factory(), nil +} + +// RenderBackendInfos 列出已注册组合及其实际能力,供原生程序和WASM共用 +// 返回: map[string]BackendInfo 后端能力 +func RenderBackendInfos() map[string]BackendInfo { + backendRegistry.RLock() + factories := make(map[string]func() RenderBackends, len(backendRegistry.factories)) + for name, factory := range backendRegistry.factories { + factories[name] = factory + } + backendRegistry.RUnlock() + result := make(map[string]BackendInfo, len(factories)) + for name, factory := range factories { + result[name] = factory().Info() + } + return result +} diff --git a/ofdgo_backend_registry.go b/ofdgo_backend_registry.go deleted file mode 100644 index 1e17b55..0000000 --- a/ofdgo_backend_registry.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "fmt" - "sync" -) - -// backendRegistry 保存后端工厂,不共享有状态的后端实例 -var backendRegistry = struct { - sync.RWMutex - factories map[string]func() RenderBackends -}{factories: make(map[string]func() RenderBackends)} - -// RegisterRenderBackend 注册后端组合,工厂应返回独立实例,重复标识不会覆盖已有实现 -// 入参: name 后端标识, factory 后端工厂 -// 返回: error 无效或重复注册错误 -func RegisterRenderBackend(name string, factory func() RenderBackends) error { - if name == "" || factory == nil { - return fmt.Errorf("backend name and factory are required") - } - backendRegistry.Lock() - defer backendRegistry.Unlock() - if _, exists := backendRegistry.factories[name]; exists { - return fmt.Errorf("render backend %q already registered", name) - } - backendRegistry.factories[name] = factory - return nil -} - -// NewRenderBackends 创建已注册后端组合,各项能力通过Info报告实际提供者 -// 入参: name 已注册后端标识 -// 返回: RenderBackends 后端组合, error 未知后端错误 -func NewRenderBackends(name string) (RenderBackends, error) { - backendRegistry.RLock() - factory := backendRegistry.factories[name] - backendRegistry.RUnlock() - if factory == nil { - return RenderBackends{}, fmt.Errorf("unknown render backend %q", name) - } - return factory(), nil -} - -// RenderBackendInfos 列出已注册组合及其实际能力,供原生程序和WASM共用 -// 返回: map[string]BackendInfo 后端能力 -func RenderBackendInfos() map[string]BackendInfo { - backendRegistry.RLock() - factories := make(map[string]func() RenderBackends, len(backendRegistry.factories)) - for name, factory := range backendRegistry.factories { - factories[name] = factory - } - backendRegistry.RUnlock() - result := make(map[string]BackendInfo, len(factories)) - for name, factory := range factories { - result[name] = factory().Info() - } - return result -} diff --git a/ofdgo_compression_images.go b/ofdgo_compression_images.go index 885d6b3..5301c3a 100644 --- a/ofdgo_compression_images.go +++ b/ofdgo_compression_images.go @@ -21,6 +21,9 @@ import ( "math" ) +// compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸 +const compressionPatternImageLimit = 65536 + // compressionImages 记录图片各处使用的最大像素需求 type compressionImages struct { ctx context.Context @@ -32,6 +35,12 @@ type compressionImages struct { patternVisits map[compressionPatternKey]bool } +// compressionPatternKey 复用相同底纹的尺寸分析,平移不影响像素需求 +type compressionPatternKey struct { + pattern *Pattern + linear [4]float64 +} + // compressionImagePlan 分文档合并图片需求,版本文件的当前入口独立于其他文档处理 // 入参: ctx 取消上下文, options 压缩配置 // 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误 @@ -275,3 +284,116 @@ func (v *compressionImages) IncludeHidden() bool { return true } // SkipClips 按完整图片尺寸分析,不加载裁剪几何或文字轮廓 // 返回: bool 始终为真 func (v *compressionImages) SkipClips() bool { return true } + +// objectPatterns 统计文字和路径的填充及描边底纹,保留绘制参数继承关系 +// 入参: object 对象样式及坐标, state 继承状态 +// 返回: error 底纹分析错误 +func (v *compressionImages) objectPatterns(object PathObject, state RenderState) error { + fill, stroke := object.FillColor, (*FillColor)(object.StrokeColor) + if defaults := v.renderer.drawParamDefaults(object.DrawParam, state.Defaults); defaults != nil { + if fill == nil { + fill = defaults.FillColor + } + if stroke == nil { + stroke = (*FillColor)(defaults.StrokeColor) + } + } + if (fill == nil || fill.Pattern == nil) && (stroke == nil || stroke.Pattern == nil) { + return nil + } + matrix, _ := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state) + for _, color := range [2]*FillColor{fill, stroke} { + if color != nil && color.Pattern != nil { + if err := v.patternImages(color, matrix); err != nil { + return err + } + } + } + return nil +} + +// patternImages 按独立单元坐标汇总底纹图片及缩略图,不展开重复铺排的单元 +// 入参: color 底纹颜色, object 对象到页面矩阵 +// 返回: error 取消、循环引用或变换错误 +func (v *compressionImages) patternImages(color *FillColor, object Matrix) error { + if err := v.ctx.Err(); err != nil { + return err + } + pattern := color.Pattern + if v.patterns[pattern] { + return fmt.Errorf("cyclic pattern image reference") + } + if !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 { + return fmt.Errorf("invalid pattern cell size") + } + matrix := NewMatrix(pattern.CTM) + switch pattern.RelativeTo { + case "", "Object": + matrix = object.Multiply(matrix) + case "Page": + if v.patternPage != nil { + matrix = v.patternPage.Multiply(matrix) + } + default: + return fmt.Errorf("invalid pattern coordinate system") + } + if !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) { + return fmt.Errorf("invalid pattern transform") + } + matrix.e, matrix.f = 0, 0 + key := compressionPatternKey{pattern: pattern, linear: [4]float64{matrix.a, matrix.b, matrix.c, matrix.d}} + if v.patternVisits[key] { + return nil + } + if len(v.patternVisits) >= compressionPatternImageLimit { + return fmt.Errorf("pattern image analysis limit exceeded") + } + if v.patterns == nil { + v.patterns = make(map[*Pattern]bool) + v.patternVisits = make(map[compressionPatternKey]bool) + } + v.patternVisits[key] = true + v.patterns[pattern] = true + defer delete(v.patterns, pattern) + if pattern.CellContent.Thumbnail != "" { + id := editorResourceID(pattern.CellContent.Thumbnail) + if id == "" { + return fmt.Errorf("invalid pattern thumbnail resource") + } + size := compressionImageSize(pattern.Width*math.Hypot(matrix.a, matrix.b), pattern.Height*math.Hypot(matrix.c, matrix.d), v.dpi) + if err := v.recordSize([2]string{id}, size); err != nil { + return err + } + } + columns, rows := 1, 1 + switch pattern.ReflectMethod { + case "", "Normal": + case "Column": + columns = 2 + case "Row": + rows = 2 + case "RowAndColumn": + columns, rows = 2, 2 + default: + return fmt.Errorf("invalid pattern reflection") + } + parent := v.patternPage + defer func() { v.patternPage = parent }() + solid := FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace} + defaults := &DrawParam{FillColor: &solid, StrokeColor: (*StrokeColor)(&solid)} + for column := range columns { + for row := range rows { + cell := matrix.Multiply(Matrix{a: float64(1 - 2*column), d: float64(1 - 2*row)}) + v.patternPage = &cell + for member := range pattern.CellContent.objects() { + if err := v.ctx.Err(); err != nil { + return err + } + if err := v.renderer.WalkObject(&member, RenderState{Defaults: defaults, Parent: &cell, BoundaryInCTM: true}, v); err != nil { + return err + } + } + } + } + return nil +} diff --git a/ofdgo_compression_pattern_images.go b/ofdgo_compression_pattern_images.go deleted file mode 100644 index eb8f667..0000000 --- a/ofdgo_compression_pattern_images.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "fmt" - "math" -) - -// compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸 -const compressionPatternImageLimit = 65536 - -// compressionPatternKey 复用相同底纹的尺寸分析,平移不影响像素需求 -type compressionPatternKey struct { - pattern *Pattern - linear [4]float64 -} - -// objectPatterns 统计文字和路径的填充及描边底纹,保留绘制参数继承关系 -// 入参: object 对象样式及坐标, state 继承状态 -// 返回: error 底纹分析错误 -func (v *compressionImages) objectPatterns(object PathObject, state RenderState) error { - fill, stroke := object.FillColor, (*FillColor)(object.StrokeColor) - if defaults := v.renderer.drawParamDefaults(object.DrawParam, state.Defaults); defaults != nil { - if fill == nil { - fill = defaults.FillColor - } - if stroke == nil { - stroke = (*FillColor)(defaults.StrokeColor) - } - } - if (fill == nil || fill.Pattern == nil) && (stroke == nil || stroke.Pattern == nil) { - return nil - } - matrix, _ := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state) - for _, color := range [2]*FillColor{fill, stroke} { - if color != nil && color.Pattern != nil { - if err := v.patternImages(color, matrix); err != nil { - return err - } - } - } - return nil -} - -// patternImages 按独立单元坐标汇总底纹图片及缩略图,不展开重复铺排的单元 -// 入参: color 底纹颜色, object 对象到页面矩阵 -// 返回: error 取消、循环引用或变换错误 -func (v *compressionImages) patternImages(color *FillColor, object Matrix) error { - if err := v.ctx.Err(); err != nil { - return err - } - pattern := color.Pattern - if v.patterns[pattern] { - return fmt.Errorf("cyclic pattern image reference") - } - if !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 { - return fmt.Errorf("invalid pattern cell size") - } - matrix := NewMatrix(pattern.CTM) - switch pattern.RelativeTo { - case "", "Object": - matrix = object.Multiply(matrix) - case "Page": - if v.patternPage != nil { - matrix = v.patternPage.Multiply(matrix) - } - default: - return fmt.Errorf("invalid pattern coordinate system") - } - if !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) { - return fmt.Errorf("invalid pattern transform") - } - matrix.e, matrix.f = 0, 0 - key := compressionPatternKey{pattern: pattern, linear: [4]float64{matrix.a, matrix.b, matrix.c, matrix.d}} - if v.patternVisits[key] { - return nil - } - if len(v.patternVisits) >= compressionPatternImageLimit { - return fmt.Errorf("pattern image analysis limit exceeded") - } - if v.patterns == nil { - v.patterns = make(map[*Pattern]bool) - v.patternVisits = make(map[compressionPatternKey]bool) - } - v.patternVisits[key] = true - v.patterns[pattern] = true - defer delete(v.patterns, pattern) - if pattern.CellContent.Thumbnail != "" { - id := editorResourceID(pattern.CellContent.Thumbnail) - if id == "" { - return fmt.Errorf("invalid pattern thumbnail resource") - } - size := compressionImageSize(pattern.Width*math.Hypot(matrix.a, matrix.b), pattern.Height*math.Hypot(matrix.c, matrix.d), v.dpi) - if err := v.recordSize([2]string{id}, size); err != nil { - return err - } - } - columns, rows := 1, 1 - switch pattern.ReflectMethod { - case "", "Normal": - case "Column": - columns = 2 - case "Row": - rows = 2 - case "RowAndColumn": - columns, rows = 2, 2 - default: - return fmt.Errorf("invalid pattern reflection") - } - parent := v.patternPage - defer func() { v.patternPage = parent }() - solid := FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace} - defaults := &DrawParam{FillColor: &solid, StrokeColor: (*StrokeColor)(&solid)} - for column := range columns { - for row := range rows { - cell := matrix.Multiply(Matrix{a: float64(1 - 2*column), d: float64(1 - 2*row)}) - v.patternPage = &cell - for member := range pattern.CellContent.objects() { - if err := v.ctx.Err(); err != nil { - return err - } - if err := v.renderer.WalkObject(&member, RenderState{Defaults: defaults, Parent: &cell, BoundaryInCTM: true}, v); err != nil { - return err - } - } - } - } - return nil -} diff --git a/ofdgo_editor_resource_graph.go b/ofdgo_editor_resource_graph.go deleted file mode 100644 index 1602300..0000000 --- a/ofdgo_editor_resource_graph.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "bufio" - "bytes" - "context" - "encoding/xml" - "io" -) - -// editorResourceElement 返回标准资源集合中的声明元素名称 -// 入参: group 资源集合名称 -// 返回: string 声明元素名称,未知集合返回空值 -func editorResourceElement(group string) string { - switch group { - case "Fonts": - return "Font" - case "MultiMedias": - return "MultiMedia" - case "ColorSpaces": - return "ColorSpace" - case "DrawParams": - return "DrawParam" - case "CompositeGraphicUnits": - return "CompositeGraphicUnit" - } - return "" -} - -// editorResourceDefinitions 单次流式读取资源声明依赖,保留历史索引和非图像媒体 -// 入参: ctx 取消上下文, data 资源索引内容, name 文件路径, source 扫描结果, preserved 是否保留全部声明 -// 返回: *editorResourceRefs 含声明依赖的引用,无法安全分析时为空, error 解析或取消错误 -func editorResourceDefinitions(ctx context.Context, data []byte, name string, source *editorResourceRefs, preserved bool) (*editorResourceRefs, error) { - result := *source - result.ids, result.fonts = make(map[string]bool), nil - result.definitions = make(map[string]*editorResourceRefs) - outer := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} - scan := editorReferenceScan{ctx: ctx, refs: outer, name: name, safe: true} - input := bufio.NewReader(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) - if found, err := editorXMLPrefix(input); err != nil || !found { - return nil, err - } - decoder := xml.NewDecoder(input) - for { - if err := ctx.Err(); err != nil { - return nil, err - } - token, err := decoder.Token() - if err == io.EOF { - if !scan.seen || len(scan.stack) != 0 { - return nil, nil - } - return &result, ctx.Err() - } - if err != nil { - return nil, err - } - depth := len(scan.stack) - if node, ok := token.(xml.StartElement); ok { - if depth == 0 && node.Name.Local != "Res" || depth == 1 && editorResourceElement(node.Name.Local) == "" { - return nil, nil - } - if depth == 2 { - kind := editorResourceElement(scan.stack[1]) - var id, mediaType string - for _, attr := range node.Attr { - if attr.Name.Space == "" { - switch attr.Name.Local { - case "ID": - id = editorResourceID(attr.Value) - case "Type": - mediaType = attr.Value - } - } - } - if node.Name.Local != kind || id == "" || result.definitions[id] != nil { - return nil, nil - } - refs := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} - result.definitions[id] = refs - scan.refs = refs - if preserved || kind == "MultiMedia" && mediaType != "Image" { - result.ids[id] = true - } - } - } - if !scan.accept(token) { - return nil, ctx.Err() - } - if _, end := token.(xml.EndElement); end && depth == 3 { - scan.refs = outer - } - } -} diff --git a/ofdgo_editor_resource_scan.go b/ofdgo_editor_resource_scan.go index d8afc3d..dd34997 100644 --- a/ofdgo_editor_resource_scan.go +++ b/ofdgo_editor_resource_scan.go @@ -15,14 +15,22 @@ package ofdgo import ( + "bufio" "bytes" "context" + "encoding/xml" "io" "maps" "slices" "strings" ) +// editorResourceScope 记录资源索引的文档归属链,引用集合只读 +type editorResourceScope struct { + parent int + refs *editorResourceRefs +} + // scanEditorResources 优先扫描结构依赖,原始数据不按XML解析,孤立文件仍保留引用检查 // 入参: ctx 取消上下文, reader 原包, parts 输出改动, generated 自产引用, names 扫描范围, progress 扫描进度, collect 是否保留各文件的资源引用 // 返回: *editorResourceRefs 全部引用, []string 资源索引, map[string]*editorResourceRefs 文件引用, bool 是否可安全优化, error 读取或取消错误 @@ -147,3 +155,198 @@ func scanEditorResources(ctx context.Context, reader *Reader, parts map[string][ } return refs, resources, references, true, ctx.Err() } + +// editorResourceElement 返回标准资源集合中的声明元素名称 +// 入参: group 资源集合名称 +// 返回: string 声明元素名称,未知集合返回空值 +func editorResourceElement(group string) string { + switch group { + case "Fonts": + return "Font" + case "MultiMedias": + return "MultiMedia" + case "ColorSpaces": + return "ColorSpace" + case "DrawParams": + return "DrawParam" + case "CompositeGraphicUnits": + return "CompositeGraphicUnit" + } + return "" +} + +// editorResourceDefinitions 单次流式读取资源声明依赖,保留历史索引和非图像媒体 +// 入参: ctx 取消上下文, data 资源索引内容, name 文件路径, source 扫描结果, preserved 是否保留全部声明 +// 返回: *editorResourceRefs 含声明依赖的引用,无法安全分析时为空, error 解析或取消错误 +func editorResourceDefinitions(ctx context.Context, data []byte, name string, source *editorResourceRefs, preserved bool) (*editorResourceRefs, error) { + result := *source + result.ids, result.fonts = make(map[string]bool), nil + result.definitions = make(map[string]*editorResourceRefs) + outer := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} + scan := editorReferenceScan{ctx: ctx, refs: outer, name: name, safe: true} + input := bufio.NewReader(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) + if found, err := editorXMLPrefix(input); err != nil || !found { + return nil, err + } + decoder := xml.NewDecoder(input) + for { + if err := ctx.Err(); err != nil { + return nil, err + } + token, err := decoder.Token() + if err == io.EOF { + if !scan.seen || len(scan.stack) != 0 { + return nil, nil + } + return &result, ctx.Err() + } + if err != nil { + return nil, err + } + depth := len(scan.stack) + if node, ok := token.(xml.StartElement); ok { + if depth == 0 && node.Name.Local != "Res" || depth == 1 && editorResourceElement(node.Name.Local) == "" { + return nil, nil + } + if depth == 2 { + kind := editorResourceElement(scan.stack[1]) + var id, mediaType string + for _, attr := range node.Attr { + if attr.Name.Space == "" { + switch attr.Name.Local { + case "ID": + id = editorResourceID(attr.Value) + case "Type": + mediaType = attr.Value + } + } + } + if node.Name.Local != kind || id == "" || result.definitions[id] != nil { + return nil, nil + } + refs := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} + result.definitions[id] = refs + scan.refs = refs + if preserved || kind == "MultiMedia" && mediaType != "Image" { + result.ids[id] = true + } + } + } + if !scan.accept(token) { + return nil, ctx.Err() + } + if _, end := token.(xml.EndElement); end && depth == 3 { + scan.refs = outer + } + } +} + +// resourceReferenceScopes 按文档归集引用,归属相同的资源索引复用只读结果 +// 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 文件引用, selected 限定的文档入口,空值统计全部文档 +// 返回: map[string]*editorResourceRefs 各资源索引的引用,依赖不完整或引用无法归属时为空, error 取消错误 +func resourceReferenceScopes(ctx context.Context, documents map[string]bool, resources []string, references map[string]*editorResourceRefs, selected string) (map[string]*editorResourceRefs, error) { + indexes := make(map[string]bool, len(resources)) + for _, name := range resources { + indexes[name] = true + } + used := make(map[string]bool) + owners := make(map[string]int) + groups := []editorResourceScope{{}} + groupIDs := make(map[editorResourceScope]int) + for _, entry := range slices.Sorted(maps.Keys(documents)) { + scope := map[string]bool{entry: true} + queue := []string{entry} + refs := &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} + definitions := make(map[string]*editorResourceRefs) + for position := 0; position < len(queue); position++ { + if err := ctx.Err(); err != nil { + return nil, err + } + name := queue[position] + source := references[name] + if source == nil { + return nil, ctx.Err() + } + used[name] = true + if selected == "" || selected == entry { + refs.merge(&editorResourceRefs{ids: source.ids, fonts: source.fonts}) + for id, definition := range source.definitions { + if definitions[id] != nil { + return nil, nil + } + definitions[id] = definition + } + } + for target, role := range source.roles { + if role&packageReferenceXML != 0 && !scope[target] { + scope[target] = true + queue = append(queue, target) + } + } + } + if selected != "" && selected != entry { + continue + } + var pending []string + if len(definitions) != 0 { + pending = slices.Collect(maps.Keys(refs.ids)) + } + for position := 0; position < len(pending); position++ { + if err := ctx.Err(); err != nil { + return nil, err + } + if definition := definitions[pending[position]]; definition != nil { + for id := range definition.ids { + if !refs.ids[id] { + pending = append(pending, id) + } + } + refs.merge(&editorResourceRefs{ids: definition.ids, fonts: definition.fonts}) + } + } + for name := range scope { + if err := ctx.Err(); err != nil { + return nil, err + } + if !indexes[name] { + continue + } + key := editorResourceScope{parent: owners[name], refs: refs} + id := groupIDs[key] + if id == 0 { + id = len(groups) + groups = append(groups, key) + groupIDs[key] = id + } + owners[name] = id + } + } + for name, refs := range references { + if !used[name] && (len(refs.ids) != 0 || len(refs.fonts) != 0 || indexes[name]) { + return nil, nil + } + } + result := make(map[string]*editorResourceRefs, len(owners)) + merged := make(map[int]*editorResourceRefs) + for name, id := range owners { + if err := ctx.Err(); err != nil { + return nil, err + } + refs := merged[id] + if refs == nil { + refs = groups[id].refs + if groups[id].parent != 0 { + refs = &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} + for next := id; next != 0; next = groups[next].parent { + if err := ctx.Err(); err != nil { + return nil, err + } + refs.merge(groups[next].refs) + } + } + merged[id] = refs + } + result[name] = refs + } + return result, ctx.Err() +} diff --git a/ofdgo_editor_resource_scope.go b/ofdgo_editor_resource_scope.go deleted file mode 100644 index 608498b..0000000 --- a/ofdgo_editor_resource_scope.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "context" - "maps" - "slices" -) - -// editorResourceScope 记录资源索引的文档归属链,引用集合只读 -type editorResourceScope struct { - parent int - refs *editorResourceRefs -} - -// resourceReferenceScopes 按文档归集引用,归属相同的资源索引复用只读结果 -// 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 文件引用, selected 限定的文档入口,空值统计全部文档 -// 返回: map[string]*editorResourceRefs 各资源索引的引用,依赖不完整或引用无法归属时为空, error 取消错误 -func resourceReferenceScopes(ctx context.Context, documents map[string]bool, resources []string, references map[string]*editorResourceRefs, selected string) (map[string]*editorResourceRefs, error) { - indexes := make(map[string]bool, len(resources)) - for _, name := range resources { - indexes[name] = true - } - used := make(map[string]bool) - owners := make(map[string]int) - groups := []editorResourceScope{{}} - groupIDs := make(map[editorResourceScope]int) - for _, entry := range slices.Sorted(maps.Keys(documents)) { - scope := map[string]bool{entry: true} - queue := []string{entry} - refs := &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} - definitions := make(map[string]*editorResourceRefs) - for position := 0; position < len(queue); position++ { - if err := ctx.Err(); err != nil { - return nil, err - } - name := queue[position] - source := references[name] - if source == nil { - return nil, ctx.Err() - } - used[name] = true - if selected == "" || selected == entry { - refs.merge(&editorResourceRefs{ids: source.ids, fonts: source.fonts}) - for id, definition := range source.definitions { - if definitions[id] != nil { - return nil, nil - } - definitions[id] = definition - } - } - for target, role := range source.roles { - if role&packageReferenceXML != 0 && !scope[target] { - scope[target] = true - queue = append(queue, target) - } - } - } - if selected != "" && selected != entry { - continue - } - var pending []string - if len(definitions) != 0 { - pending = slices.Collect(maps.Keys(refs.ids)) - } - for position := 0; position < len(pending); position++ { - if err := ctx.Err(); err != nil { - return nil, err - } - if definition := definitions[pending[position]]; definition != nil { - for id := range definition.ids { - if !refs.ids[id] { - pending = append(pending, id) - } - } - refs.merge(&editorResourceRefs{ids: definition.ids, fonts: definition.fonts}) - } - } - for name := range scope { - if err := ctx.Err(); err != nil { - return nil, err - } - if !indexes[name] { - continue - } - key := editorResourceScope{parent: owners[name], refs: refs} - id := groupIDs[key] - if id == 0 { - id = len(groups) - groups = append(groups, key) - groupIDs[key] = id - } - owners[name] = id - } - } - for name, refs := range references { - if !used[name] && (len(refs.ids) != 0 || len(refs.fonts) != 0 || indexes[name]) { - return nil, nil - } - } - result := make(map[string]*editorResourceRefs, len(owners)) - merged := make(map[int]*editorResourceRefs) - for name, id := range owners { - if err := ctx.Err(); err != nil { - return nil, err - } - refs := merged[id] - if refs == nil { - refs = groups[id].refs - if groups[id].parent != 0 { - refs = &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} - for next := id; next != 0; next = groups[next].parent { - if err := ctx.Err(); err != nil { - return nil, err - } - refs.merge(groups[next].refs) - } - } - merged[id] = refs - } - result[name] = refs - } - return result, ctx.Err() -} diff --git a/ofdgo_editor_selection.go b/ofdgo_editor_selection.go index e7e4c3d..5cf54da 100644 --- a/ofdgo_editor_selection.go +++ b/ofdgo_editor_selection.go @@ -20,6 +20,15 @@ import ( "slices" ) +// ObjectSelection 保存正文与注解的独立快照,共享当前编辑器的不可变资源 +// 来源修改、删除或撤销不改变快照,不用于跨文档传输 +type ObjectSelection struct { + editor *Editor + document editorDocumentIdentity + objects []GraphicObject + annotations *AnnotationSelection +} + // SelectionBounds 获取正文与注解混合选区的实际范围,复用同一次度量的资源缓存 // 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复 // 返回: Box 页面毫米范围, error 标识或度量错误 @@ -358,3 +367,56 @@ func (e *Editor) transformSelection(page int, objects, annotations []string, mat return nil }) } + +// CaptureSelection 捕获正文与注解,不修改文档或历史 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复 +// 返回: *ObjectSelection 独立快照, error 标识或资源错误 +func (e *Editor) CaptureSelection(page int, objects, annotations []string) (*ObjectSelection, error) { + items, err := e.Objects(page, objects) + if err != nil { + return nil, err + } + selection := &ObjectSelection{editor: e, document: e.documentIdentity(), objects: items} + if len(annotations) != 0 { + selection.annotations, err = e.CaptureAnnotations(page, annotations) + if err != nil { + return nil, err + } + } + return selection, nil +} + +// PasteSelection 原子粘贴正文与注解并平移,失败回滚,一次撤销恢复全部 +// 入参: page 目标页面索引, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米 +// 返回: []string 新正文标识, []string 新注解标识, error 标识、资源或编辑错误 +func (e *Editor) PasteSelection(page int, selection *ObjectSelection, dx, dy float64) ([]string, []string, error) { + if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) || !finite(dx) || !finite(dy) { + return nil, nil, fmt.Errorf("invalid object selection or offset") + } + var objects, annotations []string + err := e.Transaction(func(edit *Editor) error { + var err error + objects, err = edit.CopyObjects(page, selection.objects, dx, dy) + if err == nil && selection.annotations != nil { + annotations, err = edit.PasteAnnotations(page, selection.annotations, dx, dy) + } + return err + }) + if err != nil { + return nil, nil, err + } + return objects, annotations, nil +} + +// PasteSelectionToComposite 将正文快照粘贴至内部容器,保留源外观,拒绝嵌套注解 +// 入参: page 目标页面索引, path 父复合路径, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米 +// 返回: []int 新成员序号, error 选区、资源或编辑错误 +func (e *Editor) PasteSelectionToComposite(page int, path ObjectPath, selection *ObjectSelection, dx, dy float64) ([]int, error) { + if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) { + return nil, fmt.Errorf("object selection belongs to another document or editor") + } + if selection.annotations != nil { + return nil, fmt.Errorf("annotations cannot be nested in a page object") + } + return e.CopyObjectsToComposite(page, path, selection.objects, dx, dy) +} diff --git a/ofdgo_editor_selection_copy.go b/ofdgo_editor_selection_copy.go deleted file mode 100644 index ee19245..0000000 --- a/ofdgo_editor_selection_copy.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import "fmt" - -// ObjectSelection 保存正文与注解的独立快照,共享当前编辑器的不可变资源 -// 来源修改、删除或撤销不改变快照,不用于跨文档传输 -type ObjectSelection struct { - editor *Editor - document editorDocumentIdentity - objects []GraphicObject - annotations *AnnotationSelection -} - -// CaptureSelection 捕获正文与注解,不修改文档或历史 -// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复 -// 返回: *ObjectSelection 独立快照, error 标识或资源错误 -func (e *Editor) CaptureSelection(page int, objects, annotations []string) (*ObjectSelection, error) { - items, err := e.Objects(page, objects) - if err != nil { - return nil, err - } - selection := &ObjectSelection{editor: e, document: e.documentIdentity(), objects: items} - if len(annotations) != 0 { - selection.annotations, err = e.CaptureAnnotations(page, annotations) - if err != nil { - return nil, err - } - } - return selection, nil -} - -// PasteSelection 原子粘贴正文与注解并平移,失败回滚,一次撤销恢复全部 -// 入参: page 目标页面索引, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米 -// 返回: []string 新正文标识, []string 新注解标识, error 标识、资源或编辑错误 -func (e *Editor) PasteSelection(page int, selection *ObjectSelection, dx, dy float64) ([]string, []string, error) { - if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) || !finite(dx) || !finite(dy) { - return nil, nil, fmt.Errorf("invalid object selection or offset") - } - var objects, annotations []string - err := e.Transaction(func(edit *Editor) error { - var err error - objects, err = edit.CopyObjects(page, selection.objects, dx, dy) - if err == nil && selection.annotations != nil { - annotations, err = edit.PasteAnnotations(page, selection.annotations, dx, dy) - } - return err - }) - if err != nil { - return nil, nil, err - } - return objects, annotations, nil -} - -// PasteSelectionToComposite 将正文快照粘贴至内部容器,保留源外观,拒绝嵌套注解 -// 入参: page 目标页面索引, path 父复合路径, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米 -// 返回: []int 新成员序号, error 选区、资源或编辑错误 -func (e *Editor) PasteSelectionToComposite(page int, path ObjectPath, selection *ObjectSelection, dx, dy float64) ([]int, error) { - if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) { - return nil, fmt.Errorf("object selection belongs to another document or editor") - } - if selection.annotations != nil { - return nil, fmt.Errorf("annotations cannot be nested in a page object") - } - return e.CopyObjectsToComposite(page, path, selection.objects, dx, dy) -} diff --git a/ofdgo_renderer_font_canvas.go b/ofdgo_renderer_font_canvas.go index b5b92f1..b2c83d8 100644 --- a/ofdgo_renderer_font_canvas.go +++ b/ofdgo_renderer_font_canvas.go @@ -19,12 +19,33 @@ import ( "sync" "github.com/tdewolff/canvas" + canvastext "github.com/tdewolff/canvas/text" basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态 var canvasFontLoadMu sync.Mutex +// textGlyphPathCacheKey 字形路径缓存键 +type textGlyphPathCacheKey struct { + font *canvas.Font + size float64 + fauxBold float64 + fauxItalic float64 + xOffset int32 + yOffset int32 + language string + script canvastext.Script + direction canvastext.Direction + glyph textGlyph +} + +// textGlyphPathCacheValue 字形路径缓存值 +type textGlyphPathCacheValue struct { + path *canvas.Path + width float64 +} + // loadCanvasFont 串行加载绘图字体,不改动原始字体数据 // 入参: data 字体数据, index 集合索引, style 字体样式 // 返回: *canvas.Font 绘图字体, error 加载错误 @@ -58,3 +79,161 @@ func decodeFontContainer(data []byte) ([]byte, error) { } return basefont.SerializeOTF(loaded.Tables) } + +// loadFont 加载字体,绘图别名按数据和样式隔离,不改动原始字体名称 +// 入参: fontID 字体ID +// 返回: *canvas.FontFamily 字体族 +func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { + if ff, ok := r.canvasState().fontMap[fontID]; ok { + return ff + } + resolved, err := r.PrepareFont(fontID) + if err != nil { + r.renderError = err + return nil + } + of := r.Reader.fontDefinition(fontID) + if len(resolved.Data) == 0 { + return nil + } + if of == nil { + of = &Font{ID: fontID, FontName: "OFDGoFallback"} + } + fontData := resolved.Data + fontStyle := canvasFontStyle(of) + if resolved.digest == ([32]byte{}) { + resolved.digest = r.fontDigest(fontData) + } + key := canvasFontKey{digest: resolved.digest, name: of.FontName, style: fontStyle} + cache := r.canvasFontFamilies() + if ff, ok := cache.get(key); ok { + r.canvasState().fontMap[fontID] = ff + return ff + } + ff := canvas.NewFontFamily(fmt.Sprintf("ofdgo-%x-%d", resolved.digest, fontStyle)) + canvasFontLoadMu.Lock() + err = ff.LoadFont(fontData, 0, fontStyle) + canvasFontLoadMu.Unlock() + if err != nil { + r.renderError = err + return nil + } + r.canvasState().fontMap[fontID] = ff + cache.put(key, ff, min(len(fontData), cache.limit)*8+256) + return ff +} + +// canvasFontStyle 获取Canvas字体样式 +// 入参: font OFD字体定义 +// 返回: canvas.FontStyle Canvas字体样式 +func canvasFontStyle(font *Font) canvas.FontStyle { + var style canvas.FontStyle + if font.Bold { + style |= canvas.FontBold + } + if font.Italic { + style |= canvas.FontItalic + } + return style +} + +// textGlyphWidth 获取绘制字形宽度 +// 入参: face 字体, glyph 绘制字形 +// 返回: float64 字形宽度 +func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 { + if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF { + return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID))) + } + return face.TextWidth(glyph.Text) +} + +// textGlyphPath 获取绘制字形路径 +// 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器 +// 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误 +func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *basefont.Outliner) (*canvas.Path, float64, error) { + p := &canvas.Path{} + width := 0.0 + if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF { + x, y := face.XOffset, face.YOffset + for _, shaped := range face.Glyphs(glyph.Text) { + part := &canvas.Path{} + if err := outlines.Path(part, shaped.ID, face.MmPerEm); err != nil { + return nil, 0, err + } + p = p.Append(part.Translate(face.MmPerEm*float64(x+shaped.XOffset), face.MmPerEm*float64(y+shaped.YOffset))) + x, y = x+shaped.XAdvance, y+shaped.YAdvance + } + width = face.MmPerEm * float64(x) + } else { + glyphID := uint16(glyph.GlyphID) + if err := outlines.Path(p, glyphID, face.MmPerEm); err != nil { + return nil, 0, err + } + width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID)) + } + if face.FauxBold != 0 { + p = p.Offset(face.FauxBold*face.Size, canvas.Tolerance) + } + if face.FauxItalic != 0 { + p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0)) + } + return p, width, nil +} + +// cachedTextGlyphPath 获取缓存的字形路径 +// 入参: face 字体, glyph 绘制字形 +// 返回: *canvas.Path 字形路径, float64 字形宽度 +func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) { + key := textGlyphPathCacheKey{ + font: face.Font, + size: face.Size, + fauxBold: face.FauxBold, + fauxItalic: face.FauxItalic, + xOffset: face.XOffset, + yOffset: face.YOffset, + language: face.Language, + script: face.Script, + direction: face.Direction, + glyph: glyph, + } + if cached, ok := r.canvasState().textGlyphPathCache.get(key); ok { + return cached.path, cached.width + } + outlines := r.canvasState().fontOutlines[face.Font] + if outlines == nil { + cache := r.canvasFontOutlines() + outlines, _ = cache.get(face.Font) + if outlines == nil { + metrics, err := basefont.ParseSFNTTables(face.Font.Tables) + if err != nil { + r.renderError = err + return &canvas.Path{}, 0 + } + outlines = &basefont.Outliner{Font: metrics} + cost := uint64(512) + for _, data := range metrics.Tables { + cost += uint64(len(data)) * 8 + } + cost += uint64(metrics.CmapEntries()) * 48 + cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1))) + } + r.canvasState().fontOutlines[face.Font] = outlines + } + path, width, err := textGlyphPath(face, glyph, outlines) + if err != nil { + r.renderError = err + return &canvas.Path{}, 0 + } + r.canvasState().textGlyphPathCache.put(key, textGlyphPathCacheValue{path: path, width: width}, 256+len(path.Data())*8+len(glyph.Text)+len(face.Language)) + return path, width +} + +// textMatrix 获取文本字形变换矩阵 +// 入参: ctm OFD变换矩阵 +// 返回: canvas.Matrix 画布变换矩阵 +func textMatrix(ctm Matrix) canvas.Matrix { + return canvas.Matrix{ + {ctm.a, -ctm.c, 0}, + {-ctm.b, ctm.d, 0}, + } +} diff --git a/ofdgo_renderer_font_loader_canvas.go b/ofdgo_renderer_font_loader_canvas.go deleted file mode 100644 index 7c08d68..0000000 --- a/ofdgo_renderer_font_loader_canvas.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "fmt" - - "github.com/tdewolff/canvas" -) - -// loadFont 加载字体,绘图别名按数据和样式隔离,不改动原始字体名称 -// 入参: fontID 字体ID -// 返回: *canvas.FontFamily 字体族 -func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { - if ff, ok := r.canvasState().fontMap[fontID]; ok { - return ff - } - resolved, err := r.PrepareFont(fontID) - if err != nil { - r.renderError = err - return nil - } - of := r.Reader.fontDefinition(fontID) - if len(resolved.Data) == 0 { - return nil - } - if of == nil { - of = &Font{ID: fontID, FontName: "OFDGoFallback"} - } - fontData := resolved.Data - fontStyle := canvasFontStyle(of) - if resolved.digest == ([32]byte{}) { - resolved.digest = r.fontDigest(fontData) - } - key := canvasFontKey{digest: resolved.digest, name: of.FontName, style: fontStyle} - cache := r.canvasFontFamilies() - if ff, ok := cache.get(key); ok { - r.canvasState().fontMap[fontID] = ff - return ff - } - ff := canvas.NewFontFamily(fmt.Sprintf("ofdgo-%x-%d", resolved.digest, fontStyle)) - canvasFontLoadMu.Lock() - err = ff.LoadFont(fontData, 0, fontStyle) - canvasFontLoadMu.Unlock() - if err != nil { - r.renderError = err - return nil - } - r.canvasState().fontMap[fontID] = ff - cache.put(key, ff, min(len(fontData), cache.limit)*8+256) - return ff -} - -// canvasFontStyle 获取Canvas字体样式 -// 入参: font OFD字体定义 -// 返回: canvas.FontStyle Canvas字体样式 -func canvasFontStyle(font *Font) canvas.FontStyle { - var style canvas.FontStyle - if font.Bold { - style |= canvas.FontBold - } - if font.Italic { - style |= canvas.FontItalic - } - return style -} diff --git a/ofdgo_renderer_glyph_canvas.go b/ofdgo_renderer_glyph_canvas.go deleted file mode 100644 index 7b24222..0000000 --- a/ofdgo_renderer_glyph_canvas.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "github.com/tdewolff/canvas" - canvastext "github.com/tdewolff/canvas/text" - basefont "github.com/xiaoqidun/ofdgo/internal/base/font" -) - -// textGlyphPathCacheKey 字形路径缓存键 -type textGlyphPathCacheKey struct { - font *canvas.Font - size float64 - fauxBold float64 - fauxItalic float64 - xOffset int32 - yOffset int32 - language string - script canvastext.Script - direction canvastext.Direction - glyph textGlyph -} - -// textGlyphPathCacheValue 字形路径缓存值 -type textGlyphPathCacheValue struct { - path *canvas.Path - width float64 -} - -// textGlyphWidth 获取绘制字形宽度 -// 入参: face 字体, glyph 绘制字形 -// 返回: float64 字形宽度 -func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 { - if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF { - return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID))) - } - return face.TextWidth(glyph.Text) -} - -// textGlyphPath 获取绘制字形路径 -// 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器 -// 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误 -func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *basefont.Outliner) (*canvas.Path, float64, error) { - p := &canvas.Path{} - width := 0.0 - if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF { - x, y := face.XOffset, face.YOffset - for _, shaped := range face.Glyphs(glyph.Text) { - part := &canvas.Path{} - if err := outlines.Path(part, shaped.ID, face.MmPerEm); err != nil { - return nil, 0, err - } - p = p.Append(part.Translate(face.MmPerEm*float64(x+shaped.XOffset), face.MmPerEm*float64(y+shaped.YOffset))) - x, y = x+shaped.XAdvance, y+shaped.YAdvance - } - width = face.MmPerEm * float64(x) - } else { - glyphID := uint16(glyph.GlyphID) - if err := outlines.Path(p, glyphID, face.MmPerEm); err != nil { - return nil, 0, err - } - width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID)) - } - if face.FauxBold != 0 { - p = p.Offset(face.FauxBold*face.Size, canvas.Tolerance) - } - if face.FauxItalic != 0 { - p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0)) - } - return p, width, nil -} - -// cachedTextGlyphPath 获取缓存的字形路径 -// 入参: face 字体, glyph 绘制字形 -// 返回: *canvas.Path 字形路径, float64 字形宽度 -func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) { - key := textGlyphPathCacheKey{ - font: face.Font, - size: face.Size, - fauxBold: face.FauxBold, - fauxItalic: face.FauxItalic, - xOffset: face.XOffset, - yOffset: face.YOffset, - language: face.Language, - script: face.Script, - direction: face.Direction, - glyph: glyph, - } - if cached, ok := r.canvasState().textGlyphPathCache.get(key); ok { - return cached.path, cached.width - } - outlines := r.canvasState().fontOutlines[face.Font] - if outlines == nil { - cache := r.canvasFontOutlines() - outlines, _ = cache.get(face.Font) - if outlines == nil { - metrics, err := basefont.ParseSFNTTables(face.Font.Tables) - if err != nil { - r.renderError = err - return &canvas.Path{}, 0 - } - outlines = &basefont.Outliner{Font: metrics} - cost := uint64(512) - for _, data := range metrics.Tables { - cost += uint64(len(data)) * 8 - } - cost += uint64(metrics.CmapEntries()) * 48 - cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1))) - } - r.canvasState().fontOutlines[face.Font] = outlines - } - path, width, err := textGlyphPath(face, glyph, outlines) - if err != nil { - r.renderError = err - return &canvas.Path{}, 0 - } - r.canvasState().textGlyphPathCache.put(key, textGlyphPathCacheValue{path: path, width: width}, 256+len(path.Data())*8+len(glyph.Text)+len(face.Language)) - return path, width -} - -// textMatrix 获取文本字形变换矩阵 -// 入参: ctm OFD变换矩阵 -// 返回: canvas.Matrix 画布变换矩阵 -func textMatrix(ctm Matrix) canvas.Matrix { - return canvas.Matrix{ - {ctm.a, -ctm.c, 0}, - {-ctm.b, ctm.d, 0}, - } -} diff --git a/ofdgo_renderer_layer_canvas.go b/ofdgo_renderer_layer_canvas.go deleted file mode 100644 index 317b9af..0000000 --- a/ofdgo_renderer_layer_canvas.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import "github.com/tdewolff/canvas" - -// canvasPageVisitor 将公共页面遍历结果交给Canvas解释器 -type canvasPageVisitor struct { - renderer *Renderer - context *canvas.Context - height float64 -} - -// DrawObject 绘制叶子对象,转换页面坐标裁剪 -// 入参: object 源对象, state 继承状态 -// 返回: error 绘制错误 -func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error { - if state.Hidden { - return nil - } - clip, err := geometryToCanvasPath(state.Clip) - if err != nil { - return err - } - if clip != nil { - clip = clip.Translate(0, v.height) - } - switch object.Type { - case "TextObject": - v.renderer.renderText(v.context, object.TextObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip) - case "PathObject": - v.renderer.renderPath(v.context, object.PathObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip) - case "ImageObject": - v.renderer.renderImage(v.context, object.ImageObject, v.height, state.Parent, state.BoundaryInCTM, clip) - } - return v.renderer.renderError -} - -// DrawStamp 绘制签章外观 -// 入参: stamp 签章 -// 返回: error 绘制错误 -func (v *canvasPageVisitor) DrawStamp(stamp Stamp) error { - v.renderer.renderStamp(v.context, stamp, v.height) - return v.renderer.renderError -} - -// BeginObject 开始可编辑对象分组 -// 入参: object 源对象 -// 返回: bool 是否建立分组 -func (v *canvasPageVisitor) BeginObject(object *GraphicObject) bool { - groups, ok := v.context.Renderer.(interface { - beginObject(*GraphicObject) bool - endObject() - }) - return ok && groups.beginObject(object) -} - -// BeginAnnotation 开始注解分组 -// 入参: id 注解标识 -// 返回: bool 是否建立分组 -func (v *canvasPageVisitor) BeginAnnotation(id string) bool { - groups, ok := v.context.Renderer.(interface { - beginAnnotation(string) bool - endObject() - }) - return ok && groups.beginAnnotation(id) -} - -// EndObject 结束对象或注解分组 -func (v *canvasPageVisitor) EndObject() { - v.context.Renderer.(interface{ endObject() }).endObject() -} - -// SkipObjects 保留不可见对象的编号 -// 入参: count 对象数量 -func (v *canvasPageVisitor) SkipObjects(count int) { - if groups, ok := v.context.Renderer.(interface{ skipObjects(int) }); ok { - groups.skipObjects(count) - } -} - -// canvasRenderState 将Canvas裁剪转换为公共页面状态 -// 入参: pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 -// 返回: RenderState 公共页面状态 -func canvasRenderState(pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) RenderState { - var clip *GeometryPath - if parentClip != nil { - clip = geometryFromCanvasPath(parentClip.Copy().Translate(0, -pageH)) - } - return RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: clip} -} - -// renderCompositeGraphicUnit 通过公共遍历绘制复合图元 -// 入参: ctx 画布上下文, cgu 复合图元, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 -func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { - if err := r.walkComposite(cgu, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}, nil); err != nil { - r.renderError = err - } -} - -// renderObject 通过公共遍历绘制图形对象 -// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 -func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { - if err := r.WalkObject(obj, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}); err != nil { - r.renderError = err - } -} diff --git a/ofdgo_renderer_page_canvas.go b/ofdgo_renderer_page_canvas.go index 2bd21cb..6e175eb 100644 --- a/ofdgo_renderer_page_canvas.go +++ b/ofdgo_renderer_page_canvas.go @@ -18,6 +18,13 @@ import ( "github.com/tdewolff/canvas" ) +// canvasPageVisitor 将公共页面遍历结果交给Canvas解释器 +type canvasPageVisitor struct { + renderer *Renderer + context *canvas.Context + height float64 +} + // renderCanvasPage 渲染特定页面内容 // 入参: page 页面内容 // 返回: *canvas.Canvas 画布实例, error 错误信息 @@ -74,3 +81,98 @@ func (r *Renderer) renderCanvasPageToContext(ctx *canvas.Context, page *PageCont } return nil } + +// DrawObject 绘制叶子对象,转换页面坐标裁剪 +// 入参: object 源对象, state 继承状态 +// 返回: error 绘制错误 +func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error { + if state.Hidden { + return nil + } + clip, err := geometryToCanvasPath(state.Clip) + if err != nil { + return err + } + if clip != nil { + clip = clip.Translate(0, v.height) + } + switch object.Type { + case "TextObject": + v.renderer.renderText(v.context, object.TextObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip) + case "PathObject": + v.renderer.renderPath(v.context, object.PathObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip) + case "ImageObject": + v.renderer.renderImage(v.context, object.ImageObject, v.height, state.Parent, state.BoundaryInCTM, clip) + } + return v.renderer.renderError +} + +// DrawStamp 绘制签章外观 +// 入参: stamp 签章 +// 返回: error 绘制错误 +func (v *canvasPageVisitor) DrawStamp(stamp Stamp) error { + v.renderer.renderStamp(v.context, stamp, v.height) + return v.renderer.renderError +} + +// BeginObject 开始可编辑对象分组 +// 入参: object 源对象 +// 返回: bool 是否建立分组 +func (v *canvasPageVisitor) BeginObject(object *GraphicObject) bool { + groups, ok := v.context.Renderer.(interface { + beginObject(*GraphicObject) bool + endObject() + }) + return ok && groups.beginObject(object) +} + +// BeginAnnotation 开始注解分组 +// 入参: id 注解标识 +// 返回: bool 是否建立分组 +func (v *canvasPageVisitor) BeginAnnotation(id string) bool { + groups, ok := v.context.Renderer.(interface { + beginAnnotation(string) bool + endObject() + }) + return ok && groups.beginAnnotation(id) +} + +// EndObject 结束对象或注解分组 +func (v *canvasPageVisitor) EndObject() { + v.context.Renderer.(interface{ endObject() }).endObject() +} + +// SkipObjects 保留不可见对象的编号 +// 入参: count 对象数量 +func (v *canvasPageVisitor) SkipObjects(count int) { + if groups, ok := v.context.Renderer.(interface{ skipObjects(int) }); ok { + groups.skipObjects(count) + } +} + +// canvasRenderState 将Canvas裁剪转换为公共页面状态 +// 入参: pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 +// 返回: RenderState 公共页面状态 +func canvasRenderState(pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) RenderState { + var clip *GeometryPath + if parentClip != nil { + clip = geometryFromCanvasPath(parentClip.Copy().Translate(0, -pageH)) + } + return RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: clip} +} + +// renderCompositeGraphicUnit 通过公共遍历绘制复合图元 +// 入参: ctx 画布上下文, cgu 复合图元, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 +func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { + if err := r.walkComposite(cgu, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}, nil); err != nil { + r.renderError = err + } +} + +// renderObject 通过公共遍历绘制图形对象 +// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 +func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { + if err := r.WalkObject(obj, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}); err != nil { + r.renderError = err + } +} diff --git a/ofdgo_sign_id.go b/ofdgo_sign_id.go deleted file mode 100644 index 1dcafd1..0000000 --- a/ofdgo_sign_id.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "fmt" - "strconv" - "strings" -) - -// signatureIDSequence 签名及签章注释共用的编号上限,与文档图元编号相互独立 -type signatureIDSequence uint64 - -// observe 记录已有签章标识的编号上限,其他命名形式保持不变 -// 入参: id 已有标识 -func (s *signatureIDSequence) observe(id string) { - n, err := strconv.ParseUint(strings.TrimPrefix(id, "s"), 10, 64) - if err == nil && n > uint64(*s) { - *s = signatureIDSequence(n) - } -} - -// next 分配采用sNNN格式的签章标识 -// 返回: string 新标识, error 编号耗尽错误 -func (s *signatureIDSequence) next() (string, error) { - if uint64(*s) == ^uint64(0) { - return "", fmt.Errorf("signature ID exhausted") - } - *s++ - return "s" + strconv.FormatUint(uint64(*s), 10), nil -} diff --git a/ofdgo_sign_write.go b/ofdgo_sign_write.go index f9f7b60..d65274d 100644 --- a/ofdgo_sign_write.go +++ b/ofdgo_sign_write.go @@ -76,6 +76,9 @@ type signatureWriteInfo struct { Seal *SignatureSeal `xml:"Seal,omitempty"` } +// signatureIDSequence 签名及签章注释共用的编号上限,与文档图元编号相互独立 +type signatureIDSequence uint64 + // SignPackage 签署最终OFD包,失败时返回nil且不修改输入 // 含版本列表的文档需先通过ExtractVersion提取后签署 // 入参: data 最终包字节, options 签署选项 @@ -620,3 +623,22 @@ func signatureWriteZIP(parts map[string][]byte, compression CompressionOptions) } return out.Bytes(), nil } + +// observe 记录已有签章标识的编号上限,其他命名形式保持不变 +// 入参: id 已有标识 +func (s *signatureIDSequence) observe(id string) { + n, err := strconv.ParseUint(strings.TrimPrefix(id, "s"), 10, 64) + if err == nil && n > uint64(*s) { + *s = signatureIDSequence(n) + } +} + +// next 分配采用sNNN格式的签章标识 +// 返回: string 新标识, error 编号耗尽错误 +func (s *signatureIDSequence) next() (string, error) { + if uint64(*s) == ^uint64(0) { + return "", fmt.Errorf("signature ID exhausted") + } + *s++ + return "s" + strconv.FormatUint(uint64(*s), 10), nil +} diff --git a/ofdgo_structure_object.go b/ofdgo_structure_object.go deleted file mode 100644 index 4cc6fb3..0000000 --- a/ofdgo_structure_object.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import "context" - -// structureObjectVisitor 收集页面、复合图元及注释外观标识,绑定校验不解码字体、图片或裁剪 -type structureObjectVisitor struct { - objects map[string]bool - targets []StructureTarget - listTargets bool -} - -// StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容、复合图元成员和注释外观 -// 不解码字体和图片,返回顺序遵循页面遍历顺序,同一标识仅列出一次 -// 入参: page 从0开始的页面索引 -// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面或遍历错误 -func (r *Renderer) StructureObjects(page int) ([]StructureTarget, error) { - return r.StructureObjectsContext(context.Background(), page) -} - -// StructureObjectsContext 列出页面绑定候选,读取或遍历取消时不返回部分结果,不改变渲染选项 -// 入参: ctx 取消上下文, page 从0开始的页面索引 -// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面读取、遍历或取消错误 -func (r *Renderer) StructureObjectsContext(ctx context.Context, page int) ([]StructureTarget, error) { - content, err := r.Reader.PageContentByIndexContext(ctx, page) - if err != nil { - return nil, err - } - visitor := &structureObjectVisitor{objects: make(map[string]bool), listTargets: true} - view := *r - view.RenderAnnotations = true - view.pageContext = ctx - if err := view.WalkPage(content, visitor); err != nil { - return nil, err - } - for i := range visitor.targets { - if err := ctx.Err(); err != nil { - return nil, err - } - visitor.targets[i].Page, visitor.targets[i].PageID = page+1, content.ID - } - if err := ctx.Err(); err != nil { - return nil, err - } - return visitor.targets, nil -} - -// BeginObject 登记图元标识,不创建分组状态 -// 入参: object 页面图元 -// 返回: bool 始终为false -func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool { - id := editorResourceID(editorObjectID(*object)) - if id != "" && !v.objects[id] { - v.objects[id] = true - if !v.listTargets { - return false - } - target := StructureTarget{ID: id} - if object.Type == "TextObject" { - target.Text = object.TextObject.Text() - } - v.targets = append(v.targets, target) - } - return false -} - -// EndObject 满足分组访问接口,不维护分组栈 -func (v *structureObjectVisitor) EndObject() {} - -// BeginAnnotation 不将注释容器编号作为图元标识 -// 入参: id 注解标识 -// 返回: bool 始终为false -func (v *structureObjectVisitor) BeginAnnotation(id string) bool { return false } - -// SkipObjects 不依赖图元序号 -// 入参: count 跳过数量 -func (v *structureObjectVisitor) SkipObjects(count int) {} - -// IncludeHidden 允许绑定隐藏图元 -// 返回: bool 始终为true -func (v *structureObjectVisitor) IncludeHidden() bool { return true } - -// SkipClips 绑定校验不需要裁剪轮廓 -// 返回: bool 始终为true -func (v *structureObjectVisitor) SkipClips() bool { return true } - -// DrawObject 不绘制图元,标识已由BeginObject登记 -// 入参: object 页面图元, state 继承状态 -// 返回: error 始终为nil -func (v *structureObjectVisitor) DrawObject(object *GraphicObject, state RenderState) error { - return nil -} - -// DrawStamp 不将签章外观作为正文绑定目标 -// 入参: stamp 签章外观 -// 返回: error 始终为nil -func (v *structureObjectVisitor) DrawStamp(stamp Stamp) error { return nil } diff --git a/ofdgo_structure_target.go b/ofdgo_structure_target.go index 472523c..44b9025 100644 --- a/ofdgo_structure_target.go +++ b/ofdgo_structure_target.go @@ -29,6 +29,13 @@ type structureVisitor struct { boundsOnly bool } +// structureObjectVisitor 收集页面、复合图元及注释外观标识,绑定校验不解码字体、图片或裁剪 +type structureObjectVisitor struct { + objects map[string]bool + targets []StructureTarget + listTargets bool +} + // BeginObject 记录当前图元的标引目标,使复合图元引用覆盖其成员 // 入参: object 页面图元 // 返回: bool 是否需要配对结束通知 @@ -120,3 +127,89 @@ func (v *structureVisitor) DrawObject(object *GraphicObject, state RenderState) // 入参: stamp 签章外观 // 返回: error 取消错误 func (v *structureVisitor) DrawStamp(stamp Stamp) error { return v.ctx.Err() } + +// StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容、复合图元成员和注释外观 +// 不解码字体和图片,返回顺序遵循页面遍历顺序,同一标识仅列出一次 +// 入参: page 从0开始的页面索引 +// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面或遍历错误 +func (r *Renderer) StructureObjects(page int) ([]StructureTarget, error) { + return r.StructureObjectsContext(context.Background(), page) +} + +// StructureObjectsContext 列出页面绑定候选,读取或遍历取消时不返回部分结果,不改变渲染选项 +// 入参: ctx 取消上下文, page 从0开始的页面索引 +// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面读取、遍历或取消错误 +func (r *Renderer) StructureObjectsContext(ctx context.Context, page int) ([]StructureTarget, error) { + content, err := r.Reader.PageContentByIndexContext(ctx, page) + if err != nil { + return nil, err + } + visitor := &structureObjectVisitor{objects: make(map[string]bool), listTargets: true} + view := *r + view.RenderAnnotations = true + view.pageContext = ctx + if err := view.WalkPage(content, visitor); err != nil { + return nil, err + } + for i := range visitor.targets { + if err := ctx.Err(); err != nil { + return nil, err + } + visitor.targets[i].Page, visitor.targets[i].PageID = page+1, content.ID + } + if err := ctx.Err(); err != nil { + return nil, err + } + return visitor.targets, nil +} + +// BeginObject 登记图元标识,不创建分组状态 +// 入参: object 页面图元 +// 返回: bool 始终为false +func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool { + id := editorResourceID(editorObjectID(*object)) + if id != "" && !v.objects[id] { + v.objects[id] = true + if !v.listTargets { + return false + } + target := StructureTarget{ID: id} + if object.Type == "TextObject" { + target.Text = object.TextObject.Text() + } + v.targets = append(v.targets, target) + } + return false +} + +// EndObject 满足分组访问接口,不维护分组栈 +func (v *structureObjectVisitor) EndObject() {} + +// BeginAnnotation 不将注释容器编号作为图元标识 +// 入参: id 注解标识 +// 返回: bool 始终为false +func (v *structureObjectVisitor) BeginAnnotation(id string) bool { return false } + +// SkipObjects 不依赖图元序号 +// 入参: count 跳过数量 +func (v *structureObjectVisitor) SkipObjects(count int) {} + +// IncludeHidden 允许绑定隐藏图元 +// 返回: bool 始终为true +func (v *structureObjectVisitor) IncludeHidden() bool { return true } + +// SkipClips 绑定校验不需要裁剪轮廓 +// 返回: bool 始终为true +func (v *structureObjectVisitor) SkipClips() bool { return true } + +// DrawObject 不绘制图元,标识已由BeginObject登记 +// 入参: object 页面图元, state 继承状态 +// 返回: error 始终为nil +func (v *structureObjectVisitor) DrawObject(object *GraphicObject, state RenderState) error { + return nil +} + +// DrawStamp 不将签章外观作为正文绑定目标 +// 入参: stamp 签章外观 +// 返回: error 始终为nil +func (v *structureObjectVisitor) DrawStamp(stamp Stamp) error { return nil } diff --git a/ofdgo_text.go b/ofdgo_text.go index 307396c..884d3da 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -266,19 +266,6 @@ func textCodePositioned(textCode TextCode, xs, ys textPositionValues) bool { return multipleX || multipleY } -// textDelta 获取文本偏移量 -// 入参: deltas 偏移量数组, index 偏移量索引 -// 返回: float64 偏移量, bool 是否存在 -func textDelta(deltas []float64, index int) (float64, bool) { - if len(deltas) == 0 { - return 0, false - } - if index < len(deltas) { - return deltas[index], true - } - return deltas[len(deltas)-1], true -} - // textCodeXMLValue 将空格转义还原为字符,供XML写出层统一编码 // 入参: value OFD文字编码 // 返回: string 待写出的文字编码