From 6c14eef1eea4cf09c0335394b92ab999aa4c01ab Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 5 Oct 2026 12:49:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B8=B2=E6=9F=93=E7=BC=96=E8=BE=91):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B8=B2=E6=9F=93=E7=BC=96=E8=BE=91=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E8=B5=84=E6=BA=90=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- ofdgo_compression_resources.go | 20 ++- ofdgo_editor.go | 72 +++++---- ofdgo_editor_batch.go | 84 +++++++++- ofdgo_editor_clone.go | 123 ++++++++++++++ ofdgo_editor_history.go | 2 + ofdgo_editor_open.go | 39 ----- ofdgo_editor_package.go | 117 +++++++++++--- ofdgo_editor_pages.go | 27 +++- ofdgo_editor_reference.go | 232 +++++++++++++++++++++++++++ ofdgo_editor_resources.go | 139 +++++++--------- ofdgo_editor_stage.go | 147 +++++++++++++++++ ofdgo_font_backend.go | 27 +++- ofdgo_font_cmap.go | 185 ++++++++++++++++++--- ofdgo_font_info.go | 15 +- ofdgo_font_source.go | 10 +- ofdgo_geom_transform.go | 3 + ofdgo_pdf.go | 94 +++++++---- ofdgo_pdf_annot.go | 6 +- ofdgo_pdf_buffer.go | 119 ++++++++++++++ ofdgo_pdf_composite.go | 77 ++++++--- ofdgo_pdf_composite_draw.go | 142 +++++++++++++--- ofdgo_pdf_composite_pattern.go | 15 +- ofdgo_pdf_composite_scratch.go | 87 ++++++++++ ofdgo_pdf_font.go | 2 +- ofdgo_pdf_group.go | 43 ++--- ofdgo_pdf_mask.go | 27 +++- ofdgo_pdf_object.go | 131 ++++++++------- ofdgo_pdf_text_group.go | 13 +- ofdgo_raster.go | 7 +- ofdgo_raster_bounds.go | 121 ++++++++++++++ ofdgo_reader.go | 28 +++- ofdgo_render_text.go | 3 +- ofdgo_renderer.go | 5 +- ofdgo_renderer_font_loader_canvas.go | 8 +- ofdgo_renderer_geom_canvas.go | 10 ++ ofdgo_text_shape.go | 2 +- ofdgo_writer.go | 158 +++++++++++++++--- 39 files changed, 1926 insertions(+), 420 deletions(-) create mode 100644 ofdgo_editor_clone.go create mode 100644 ofdgo_editor_reference.go create mode 100644 ofdgo_editor_stage.go create mode 100644 ofdgo_pdf_buffer.go create mode 100644 ofdgo_pdf_composite_scratch.go create mode 100644 ofdgo_raster_bounds.go diff --git a/go.mod b/go.mod index b94c43a..aa2c0f4 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,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-20261004151751-bff53238d953 + github.com/xiaoqidun/pdfgo v0.0.0-20261005044242-0007b05b8143 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 cd292d1..8e09f4c 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-20261004151751-bff53238d953 h1:4fz7oPfhMZr8N2fWjv7Gn6+L2TInWGarQm3UGaqV+54= -github.com/xiaoqidun/pdfgo v0.0.0-20261004151751-bff53238d953/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20261005044242-0007b05b8143 h1:OELHUuQnFJHk9qyHQX2gzdMvSQNbnEyD70YeB5Eoldg= +github.com/xiaoqidun/pdfgo v0.0.0-20261005044242-0007b05b8143/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/ofdgo_compression_resources.go b/ofdgo_compression_resources.go index 9f97fce..0cac40e 100644 --- a/ofdgo_compression_resources.go +++ b/ofdgo_compression_resources.go @@ -32,6 +32,13 @@ import ( // 入参: parts 输出改动, reader 原包,可为空, removed 已移除条目 // 返回: error 读取或取消错误 func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader, removed map[string]bool) error { + return e.compressResourceReferences(parts, reader, removed, nil) +} + +// compressResourceReferences 优化资源,复用自产页面引用并流式读取暂存页面 +// 入参: parts 输出改动, reader 原包, removed 已移除条目, generated 本次自产页面及引用 +// 返回: error 读取或取消错误 +func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Reader, removed map[string]bool, generated map[string]editorGeneratedReferences) error { if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected { return nil } @@ -68,9 +75,18 @@ func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader, return nil } actual[key] = name + data, replaced := parts[name] + if known, ok := generated[name]; ok && replaced && known.refs != nil && known.matches(data) { + refs.merge(known.refs) + continue + } var input io.ReadCloser - if data, ok := parts[name]; ok { - input = io.NopCloser(bytes.NewReader(data)) + if replaced { + if known := generated[name]; data == nil && known.staged != nil { + input = known.staged.open() + } else { + input = io.NopCloser(bytes.NewReader(data)) + } } else { var err error input, err = reader.openFile(name) diff --git a/ofdgo_editor.go b/ofdgo_editor.go index aa16a9b..fa21585 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -45,30 +45,31 @@ const editorPathValidationLimit = 1024 // 阶段可重复,准备完成不代表保存成功 // 回调不可重入修改编辑器,可返回context.Canceled等调用方停止原因 type Editor struct { - OnWriteProgress func(stage string, completed, total int) error - Info DocInfo - pages []PageContent - resources []editorResource - fonts map[string]*FontResource - fontDirs []string - fontFS []fs.FS - fontRenderer *Renderer - fontMetrics map[string]FontMetrics - backends RenderBackends - images map[string]image.Point - resourceID map[editorResourceKey]string - maxID int - history []editorChange - historyIndex int - historyLimit int - revision uint64 - serial uint64 - source *editorSource - origins map[string]*editorObjectOrigin - removedPages map[string]bool - outlines []byte - encryption *encryptionState - output *outputOptimization + OnWriteProgress func(stage string, completed, total int) error + Info DocInfo + pages []PageContent + resources []editorResource + fonts map[string]*FontResource + fontDirs []string + fontFS []fs.FS + fontRenderer *Renderer + fontSourcesCache *fontSourceCache + fontMetrics map[string]FontMetrics + backends RenderBackends + images map[string]image.Point + resourceID map[editorResourceKey]string + maxID int + history []editorChange + historyIndex int + historyLimit int + revision uint64 + serial uint64 + source *editorSource + origins map[string]*editorObjectOrigin + removedPages map[string]bool + outlines []byte + encryption *encryptionState + output *outputOptimization } // editorResource 文档内嵌资源 @@ -100,6 +101,7 @@ type editorResourceKey struct { func (e *Editor) SetFontDirs(dirs ...string) { e.fontDirs = slices.Clone(dirs) e.fontRenderer = nil + e.fontSourcesCache = nil e.fontMetrics = make(map[string]FontMetrics) } @@ -108,6 +110,7 @@ func (e *Editor) SetFontDirs(dirs ...string) { func (e *Editor) SetFontFS(fsys ...fs.FS) { e.fontFS = slices.Clone(fsys) e.fontRenderer = nil + e.fontSourcesCache = nil e.fontMetrics = make(map[string]FontMetrics) } @@ -687,7 +690,12 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e // 入参: reader 当前文档快照 // 返回: *Renderer 度量器 func (e *Editor) newRenderer(reader *Reader) *Renderer { - return NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends)) + if e.fontSourcesCache == nil { + e.fontSourcesCache = newFontSourceCache() + } + renderer := NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends)) + renderer.fontSourcesCache = e.fontSourcesCache.child() + return renderer } // definition 获取独立定义资源的标识 @@ -1091,14 +1099,12 @@ func (e *Editor) prepareText(obj *TextObject) error { if obj.VScale != 0 || obj.Decoration != "" { return &EditError{Code: EditUnsupportedObject, Err: fmt.Errorf("text extensions are not supported for creation")} } - if external { - if len(obj.CGTransform) != 0 { - return fmt.Errorf("external font text cannot use glyph indices") - } - } else { - if err := validateTextGlyphs(*obj, sfnt.NumGlyphs()); err != nil { - return err - } + glyphCount := 1 << 16 + if !external { + glyphCount = int(sfnt.NumGlyphs()) + } + if err := validateTextGlyphs(*obj, glyphCount); err != nil { + return err } if !finite(obj.HScale) || obj.HScale < 0 || !finite(obj.LineWidth) || obj.LineWidth < 0 || !finite(obj.MiterLimit) || obj.MiterLimit < 0 { return fmt.Errorf("invalid text dimensions") diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index bd07025..ade7ed5 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -155,13 +155,85 @@ func (e *Editor) CopyObjectsContext(ctx context.Context, page int, objects []Gra for end < len(prepared) && e.copiedLayerStyle(objects[end]) == style { end++ } - layers[target].Objects = append(layers[target].Objects, prepared[i:end]...) + if len(layers[target].Objects) == 0 { + layers[target].Objects = prepared[i:end:end] + } else { + layers[target].Objects = append(layers[target].Objects, prepared[i:end]...) + } i = end } - e.replaceLayers(page, layers) + e.replacePreparedLayers(page, layers) return result, nil } +// appendOwnedObjects 接收本库生成对象的所有权,全部校验后提交一次撤销记录 +// 入参: ctx 取消上下文, page 目标页, objects 无原文和标识且调用方不再修改的对象 +// 返回: error 校验或取消错误,失败时不提交页面内容 +func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []GraphicObject) error { + if err := ctx.Err(); err != nil { + return err + } + if _, err := e.page(page); err != nil { + return err + } + if len(objects) == 0 { + return nil + } + if err := e.prepareSourceIDs(); err != nil { + return err + } + validation := &editorValidation{Editor: e} + for i, object := range objects { + if err := ctx.Err(); err != nil { + return err + } + if object.origin != nil || editorObjectID(object) != "" { + return fmt.Errorf("owned objects must not have identifiers or original content") + } + prepared, err := validation.prepareObject("", object) + if err != nil { + return err + } + objects[i] = prepared + } + if err := ctx.Err(); err != nil { + return err + } + for i := range objects { + id := strconv.Itoa(e.maxID + i + 1) + switch objects[i].Type { + case "TextObject": + objects[i].TextObject.ID = id + case "PathObject": + objects[i].PathObject.ID = id + case "ImageObject": + objects[i].ImageObject.ID = id + case "CompositeObject": + objects[i].CompositeGraphicUnit.ID = id + } + } + layers := copyEditorPage(e.pages[page]).Content.Layer + target := 0 + if e.originalPage(page) { + target = len(e.source.pages[e.pages[page].ID].original.Content.Layer) + } + if target < len(layers) { + target = len(layers) - 1 + } + e.maxID += len(objects) + if target == len(layers) || layers[target].DrawParam != "" { + layers = append(layers, Layer{ID: e.nextID(), Type: "Body"}) + target = len(layers) - 1 + } + if len(layers[target].Objects) == 0 { + layers[target].Objects = objects[:len(objects):len(objects)] + } else { + layers[target].Objects = append(layers[target].Objects, objects...) + } + e.replacePreparedLayers(page, layers) + return nil +} + // PageObjectInfo 批量获取页面对象能力与位置,不重复扫描同一容器 // 每次按当前页面计算,变更和撤销后无需失效旧缓存 // 入参: page 页面索引 @@ -543,12 +615,18 @@ func (e *Editor) replaceLayers(page int, layers []Layer) { return } after := copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer + e.replacePreparedLayers(page, after) +} + +// replacePreparedLayers 提交已隔离的图层容器,历史启用时仍保留独立快照 +// 入参: page 页面索引, after 已准备且不再由调用方修改的图层列表 +func (e *Editor) replacePreparedLayers(page int, after []Layer) { if e.historyLimit == 0 { e.pages[page].Content.Layer = after e.recordChange() return } - before = copyEditorPage(PageContent{Content: Content{Layer: before}}).Content.Layer + before := copyEditorPage(e.pages[page]).Content.Layer apply := func(e *Editor, layers []Layer) { e.pages[page].Content.Layer = copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer } diff --git a/ofdgo_editor_clone.go b/ofdgo_editor_clone.go new file mode 100644 index 0000000..2734a08 --- /dev/null +++ b/ofdgo_editor_clone.go @@ -0,0 +1,123 @@ +// 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 ( + "reflect" + "sync" +) + +// editorClonePlans 仅共享类型的可变字段索引,不持有文档对象或资源 +var editorClonePlans sync.Map + +// cloneEditorData 深复制编辑快照,未导出的排版及原文来源保持不可变共享 +// 入参: value 原值 +// 返回: T 独立副本 +func cloneEditorData[T any](value T) T { + source := reflect.ValueOf(value) + if !source.IsValid() { + return value + } + return cloneEditorValue(source).Interface().(T) +} + +// cloneEditorValue 复制公开指针、切片及数组,纯值数据不创建额外结构 +// 入参: value 原值 +// 返回: reflect.Value 独立副本 +func cloneEditorValue(value reflect.Value) reflect.Value { + if !value.IsValid() || !editorCloneMutable(value.Type()) { + return value + } + switch value.Kind() { + case reflect.Pointer: + if value.IsNil() { + return value + } + result := reflect.New(value.Type().Elem()) + cloneEditorInto(result.Elem(), value.Elem()) + return result + case reflect.Slice: + if value.IsNil() { + return value + } + result := reflect.MakeSlice(value.Type(), value.Len(), value.Len()) + if !editorCloneMutable(value.Type().Elem()) { + reflect.Copy(result, value) + } else { + for i := range value.Len() { + cloneEditorInto(result.Index(i), value.Index(i)) + } + } + return result + default: + result := reflect.New(value.Type()).Elem() + cloneEditorInto(result, value) + return result + } +} + +// cloneEditorInto 直接填充目标结构,避免指针及切片成员的二次结构分配 +// 入参: target 可写目标, source 原值 +func cloneEditorInto(target, source reflect.Value) { + switch source.Kind() { + case reflect.Struct: + target.Set(source) + for _, i := range editorCloneFields(source.Type()) { + cloneEditorInto(target.Field(i), source.Field(i)) + } + case reflect.Array: + target.Set(source) + if editorCloneMutable(source.Type().Elem()) { + for i := range source.Len() { + cloneEditorInto(target.Index(i), source.Index(i)) + } + } + default: + target.Set(cloneEditorValue(source)) + } +} + +// editorCloneMutable 判断类型是否含有需要隔离的公开可变成员 +// 入参: kind 数据类型 +// 返回: bool 是否需要独立复制 +func editorCloneMutable(kind reflect.Type) bool { + switch kind.Kind() { + case reflect.Pointer, reflect.Slice: + return true + case reflect.Array: + return editorCloneMutable(kind.Elem()) + case reflect.Struct: + return len(editorCloneFields(kind)) != 0 + } + return false +} + +// editorCloneFields 按类型复用公开可变字段,索引发布后保持只读 +// 入参: kind 结构类型 +// 返回: []int 可变字段索引 +func editorCloneFields(kind reflect.Type) []int { + if cached, ok := editorClonePlans.Load(kind); ok { + return cached.([]int) + } + var fields []int + for i := range kind.NumField() { + field := kind.Field(i) + if field.IsExported() && editorCloneMutable(field.Type) { + fields = append(fields, i) + } + } + actual, _ := editorClonePlans.LoadOrStore(kind, fields) + return actual.([]int) +} diff --git a/ofdgo_editor_history.go b/ofdgo_editor_history.go index e5725f2..098d63d 100644 --- a/ofdgo_editor_history.go +++ b/ofdgo_editor_history.go @@ -49,6 +49,7 @@ func (e *Editor) Transaction(edit func(*Editor) error) error { changed := e.revision != before.revision e.history, e.historyIndex, e.historyLimit = before.history, before.historyIndex, before.historyLimit e.backends, e.fontDirs, e.fontFS = before.backends, before.fontDirs, before.fontFS + e.fontSourcesCache = before.fontSourcesCache if e.sameDocument(&before) { e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics } else { @@ -159,6 +160,7 @@ func (e *Editor) restoreTransaction(state Editor) { state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit state.serial = max(e.serial, state.serial) state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS + state.fontSourcesCache = e.fontSourcesCache state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress *e = state } diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 2454125..059347e 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -912,45 +912,6 @@ func (e *Editor) editorImage(id string) (image.Point, error) { return size, nil } -// cloneEditorData 深复制编辑快照,未导出的排版及原文来源保持不可变共享 -// 入参: value 原值 -// 返回: T 独立副本 -func cloneEditorData[T any](value T) T { - return cloneEditorValue(reflect.ValueOf(value)).Interface().(T) -} - -// cloneEditorValue 复制结构体中的可变指针和切片 -// 入参: value 原值 -// 返回: reflect.Value 独立副本 -func cloneEditorValue(value reflect.Value) reflect.Value { - switch value.Kind() { - case reflect.Pointer: - if !value.IsNil() { - result := reflect.New(value.Type().Elem()) - result.Elem().Set(cloneEditorValue(value.Elem())) - return result - } - case reflect.Slice: - if !value.IsNil() { - result := reflect.MakeSlice(value.Type(), value.Len(), value.Len()) - for i := range value.Len() { - result.Index(i).Set(cloneEditorValue(value.Index(i))) - } - return result - } - case reflect.Struct: - result := reflect.New(value.Type()).Elem() - result.Set(value) - for i := range value.NumField() { - if result.Field(i).CanSet() && value.Type().Field(i).IsExported() { - result.Field(i).Set(cloneEditorValue(value.Field(i))) - } - } - return result - } - return value -} - // editorObjectXML 编码可编辑对象,包含独立命名空间声明 // 入参: object 对象 // 返回: []byte XML片段, error 错误信息 diff --git a/ofdgo_editor_package.go b/ofdgo_editor_package.go index f56cbb9..1b7e811 100644 --- a/ofdgo_editor_package.go +++ b/ofdgo_editor_package.go @@ -31,7 +31,22 @@ import ( // 入参: progress 保存进度回调,预览时为nil // 返回: map[string][]byte 替换及新增条目, error 错误信息 func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) { + return e.sourcePartsReferences(progress, nil) +} + +// sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用 +// 入参: progress 保存进度回调, generated 本次自产页面引用,可为nil +// 返回: map[string][]byte 输出条目, error 编码错误 +func (e *Editor) sourcePartsReferences(progress editorProgress, generated map[string]editorGeneratedReferences) (map[string][]byte, error) { + return e.sourcePartsPrepared(progress, generated, false) +} + +// sourcePartsPrepared 准备保存条目,按需压缩暂存自产页面 +// 入参: progress 保存进度回调, generated 本次页面及引用, stage 是否压缩暂存 +// 返回: map[string][]byte 输出条目, error 编码错误 +func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[string]editorGeneratedReferences, stage bool) (map[string][]byte, error) { parts := make(map[string][]byte) + var stager editorPageStager source := e.source reader := source.reader pageRefs := make([]Page, len(e.pages)) @@ -56,11 +71,31 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) } } else { name := e.packageName(packagePagePath("", page.ID)) - data, err := e.sourceNewPageXML(page) - if err != nil { - return nil, err + var scan *editorReferenceScan + if generated != nil { + scan = &editorReferenceScan{refs: &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}, name: name, safe: true} + } + if stage && generated != nil && !e.sourcePagePreserved(page) { + data, err := stager.encode(e, page, scan) + if err != nil { + return nil, err + } + parts[name] = nil + known := editorGeneratedReferences{staged: data} + if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 { + known.refs = scan.refs + } + generated[name] = known + } else { + data, err := e.sourcePageData(page, scan) + if err != nil { + return nil, err + } + parts[name] = data + if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 { + generated[name] = editorGeneratedReferences{data: data, refs: scan.refs} + } } - parts[name] = data pageRefs[i] = Page{ID: page.ID, BaseLoc: "/" + name} } } @@ -80,7 +115,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) }) } if len(fonts)+len(images)+len(spaces) != 0 { - data, err := encodeOFDXML(func(x *ofdXML) { x.resources(fonts, images, spaces) }) + data, err := e.encodeXML(func(x *ofdXML) { x.resources(fonts, images, spaces) }) if err != nil { return nil, err } @@ -155,7 +190,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) if original != nil { entries = append(entries, data[original.start:original.end]) } else { - item, err := encodeOFDXML(func(x *ofdXML) { + item, err := e.encodeXML(func(x *ofdXML) { x.root("Page", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: page.ID}, {Name: xml.Name{Local: "BaseLoc"}, Value: page.BaseLoc}}) x.end("Page") }) @@ -215,7 +250,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) parts[name] = updated } if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 { - if err := e.prunePageReferences(parts); err != nil { + if err := e.prunePageReferences(parts, generated); err != nil { return nil, err } } @@ -225,6 +260,10 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) if actual != name { delete(parts, name) parts[actual] = data + if known, ok := generated[name]; ok { + delete(generated, name) + generated[actual] = known + } } } } @@ -392,11 +431,14 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err // 返回: []byte 图层XML, error 错误信息 func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) { if !slices.ContainsFunc(layer.Objects, func(object GraphicObject) bool { return e.objectOrigin(editorObjectID(object)) != nil }) { - data, err := encodeOFDXML(func(x *ofdXML) { + data, err := e.encodeXML(func(x *ofdXML) { attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: layer.ID}, {Name: xml.Name{Local: "Type"}, Value: layer.Type}} attrs.add("DrawParam", layer.DrawParam) x.root("Layer", attrs) for _, object := range layer.Objects { + if x.err != nil { + return + } x.object(object, false) } x.end("Layer") @@ -429,16 +471,23 @@ func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) { // 入参: page 页面 // 返回: []byte 页面XML, error 错误信息 func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) { - preserved := false - for _, layer := range page.Content.Layer { - if slices.ContainsFunc(layer.Objects, func(object GraphicObject) bool { return e.objectOrigin(editorObjectID(object)) != nil }) { - preserved = true - break + return e.sourcePageData(page, nil) +} + +// sourcePageData 编码新增页面,仅在未保留原文时收集编码引用 +// 入参: page 页面内容, references 本次引用记录,可为nil +// 返回: []byte 页面XML, error 编码错误 +func (e *Editor) sourcePageData(page PageContent, references *editorReferenceScan) ([]byte, error) { + if !e.sourcePagePreserved(page) { + var data bytes.Buffer + x := newOFDXML(&data) + if e.output != nil { + x.ctx = e.output.ctx } - } - if !preserved { - data, err := encodeOFDXML(func(x *ofdXML) { x.page(page) }) - return bytes.TrimPrefix(data, []byte(xml.Header)), err + x.references = references + x.page(page) + err := x.finish() + return bytes.TrimPrefix(data.Bytes(), []byte(xml.Header)), err } area, err := editorXMLContainer("Area", nil, editorXMLText("PhysicalBox", page.Area.PhysicalBox)) if err != nil { @@ -458,7 +507,7 @@ func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) { } data := append(area, content...) if len(page.Actions) != 0 { - actions, err := encodeOFDXML(func(x *ofdXML) { x.actions(page.Actions) }) + actions, err := e.encodeXML(func(x *ofdXML) { x.actions(page.Actions) }) if err != nil { return nil, err } @@ -467,16 +516,29 @@ func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) { return editorXMLContainer("Page", nil, data) } +// sourcePagePreserved 判断新增页面是否仍有对象原文需要保留 +// 入参: page 页面内容 +// 返回: bool 是否包含原文对象 +func (e *Editor) sourcePagePreserved(page PageContent) bool { + for _, layer := range page.Content.Layer { + if slices.ContainsFunc(layer.Objects, func(object GraphicObject) bool { return e.objectOrigin(editorObjectID(object)) != nil }) { + return true + } + } + return false +} + // writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本 // 入参: writer 输出流, fonts 新增字体子集, progress 保存进度回调 // 返回: int64 写入字节数, error 错误信息 func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress editorProgress) (int64, error) { - parts, err := e.sourceParts(progress) + generated := make(map[string]editorGeneratedReferences) + parts, err := e.sourcePartsPrepared(progress, generated, true) if err != nil { return 0, err } maps.Copy(parts, fonts) - removed, err := e.compactSourceResources(parts, progress) + removed, err := e.compactSourceReferences(parts, progress, generated) if err != nil { return 0, err } @@ -484,7 +546,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress if removed == nil { removed = make(map[string]bool) } - if err := e.compressResourceParts(parts, reader, removed); err != nil { + if err := e.compressResourceReferences(parts, reader, removed, generated); err != nil { return 0, err } if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected { @@ -537,7 +599,11 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress } if data, ok := remaining[name]; ok { header := file.FileHeader - if err := e.writeOutputEntry(archive, header, data); err != nil { + if staged := generated[name].staged; data == nil && staged != nil { + if err := e.writeStagedPage(archive, header, staged); err != nil { + return output.count, err + } + } else if err := e.writeOutputEntry(archive, header, data); err != nil { return output.count, err } delete(remaining, name) @@ -575,7 +641,12 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress if strings.HasSuffix(strings.ToLower(name), ".xml") { method = zip.Deflate } - if err := e.writeOutputEntry(archive, zip.FileHeader{Name: name, Method: method}, remaining[name]); err != nil { + header := zip.FileHeader{Name: name, Method: method} + if staged := generated[name].staged; remaining[name] == nil && staged != nil { + if err := e.writeStagedPage(archive, header, staged); err != nil { + return output.count, err + } + } else if err := e.writeOutputEntry(archive, header, remaining[name]); err != nil { return output.count, err } } diff --git a/ofdgo_editor_pages.go b/ofdgo_editor_pages.go index d5fab85..a64d914 100644 --- a/ofdgo_editor_pages.go +++ b/ofdgo_editor_pages.go @@ -16,15 +16,16 @@ package ofdgo import ( "fmt" + "io" "maps" "strings" ) // prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档 // 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据 -// 入参: parts 输出覆盖条目 +// 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil // 返回: error 读取或解析错误 -func (e *Editor) prunePageReferences(parts map[string][]byte) error { +func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error { removed := maps.Clone(e.removedPages) if removed == nil { removed = make(map[string]bool) @@ -54,6 +55,11 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error { } seen := make(map[string]bool) for len(queue) > 0 { + if e.output != nil { + if err := e.output.ctx.Err(); err != nil { + return err + } + } name := queue[0] queue = queue[1:] if seen[strings.ToLower(name)] { @@ -67,6 +73,17 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error { if err != nil { return err } + } else if known := generated[name]; data == nil && known.staged != nil { + input := known.staged.open() + if e.output != nil { + input = imageInput{ReadCloser: input, context: e.output.ctx} + } + var err error + data, err = io.ReadAll(input) + input.Close() + if err != nil { + return err + } } root, err := parseEditorXML(data) if err != nil { @@ -75,6 +92,11 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error { var patches []editorXMLPatch var walk func(*editorXML) error walk = func(node *editorXML) error { + if e.output != nil { + if err := e.output.ctx.Err(); err != nil { + return err + } + } if node.name.Space != "" && node.name.Space != ofdNamespace && node.name.Space != "http://www.ofdspec.org" { return nil } @@ -153,6 +175,7 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error { } if len(patches) != 0 { parts[name] = editorPatchXML(data, patches) + delete(generated, name) } } return nil diff --git a/ofdgo_editor_reference.go b/ofdgo_editor_reference.go new file mode 100644 index 0000000..57299c2 --- /dev/null +++ b/ofdgo_editor_reference.go @@ -0,0 +1,232 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "bytes" + "encoding/xml" + "maps" + "strings" + "unicode/utf8" +) + +// editorGeneratedReferences 保存本次编码页面及其引用,不跨写出复用 +type editorGeneratedReferences struct { + data []byte + refs *editorResourceRefs + staged *editorStagedPage +} + +// matches 判断条目仍为本次编码结果,空条目与暂存占位不混用 +// 入参: data 当前条目数据 +// 返回: bool 是否可以复用本次引用 +func (r editorGeneratedReferences) matches(data []byte) bool { + if r.staged != nil { + return data == nil + } + return bytes.Equal(data, r.data) +} + +// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描 +type editorReferenceScan struct { + refs *editorResourceRefs + name string + base string + stack []string + fonts []*editorFontUsage + text strings.Builder + resource bool + capture bool + safe bool + seen bool +} + +// accept 共用读取和自产页面的引用判断,不将对象自身标识视为引用 +// 入参: token 已解析或已编码的XML事件 +// 返回: bool 是否仍可完整判断引用 +func (s *editorReferenceScan) accept(token xml.Token) bool { + if !s.safe { + return false + } + switch token := token.(type) { + case xml.StartElement: + if token.Name.Space != "" && token.Name.Space != ofdNamespace { + s.safe = false + return false + } + if token.Name.Local == "CustomTags" || token.Name.Local == "Extensions" || token.Name.Local == "ExtendData" || token.Name.Local == "Data" { + s.safe = false + return false + } + if len(s.stack) == 0 { + s.seen = true + s.resource = token.Name.Local == "Res" + if s.resource { + for _, attr := range token.Attr { + if attr.Name.Local == "BaseLoc" { + s.base = attr.Value + } + } + } + } + var usage *editorFontUsage + if len(s.fonts) != 0 { + usage = s.fonts[len(s.fonts)-1] + if usage != nil && (s.stack[len(s.stack)-1] == "TextCode" || s.stack[len(s.stack)-1] == "Glyphs") { + usage.unsafe = true + } + } + for _, attr := range token.Attr { + if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" || attr.Name.Local == "xmlns:ofd" || attr.Name.Space == "http://www.w3.org/XML/1998/namespace" { + continue + } + if attr.Name.Space != "" { + s.safe = false + return false + } + if attr.Name.Local != "ID" { + s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource) + } + if attr.Name.Local == "Font" { + usage = s.refs.fontUsage(attr.Value) + if token.Name.Local != "TextObject" && token.Name.Local != "Text" { + usage.unsafe = true + } + } else if attr.Name.Local == "Substitution" { + s.refs.fontUsage(attr.Value).unsafe = true + } + } + s.stack = append(s.stack, token.Name.Local) + s.fonts = append(s.fonts, usage) + s.text.Reset() + s.capture = editorResourceReferenceKind(token.Name.Local, s.resource) != editorReferenceNone || usage != nil && (token.Name.Local == "TextCode" || token.Name.Local == "Glyphs") + case xml.CharData: + if s.capture { + s.text.Write(token) + } + case xml.EndElement: + if len(s.stack) == 0 || s.stack[len(s.stack)-1] != token.Name.Local { + s.safe = false + return false + } + s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource) + if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(s.text.String()) != "" { + s.refs.fontUsage(s.text.String()).unsafe = true + } + if usage := s.fonts[len(s.fonts)-1]; usage != nil { + usage.text(token.Name.Local, s.text.String()) + } + s.stack = s.stack[:len(s.stack)-1] + s.fonts = s.fonts[:len(s.fonts)-1] + s.text.Reset() + s.capture = false + if len(s.stack) != 0 { + key, usage := s.stack[len(s.stack)-1], s.fonts[len(s.fonts)-1] + s.capture = editorResourceReferenceKind(key, s.resource) != editorReferenceNone || usage != nil && (key == "TextCode" || key == "Glyphs") + } + case xml.Directive: + s.safe = false + } + return s.safe +} + +// merge 合并本次编码已确认的引用,不共享字体用字的可变集合 +// 入参: other 独立页面引用 +func (r *editorResourceRefs) merge(other *editorResourceRefs) { + maps.Copy(r.ids, other.ids) + maps.Copy(r.files, other.files) + for id, usage := range other.fonts { + r.fontUsage(id).merge(usage, false) + } +} + +// referenceToken 记录自产OFD事件,编码会替换的字符使本页回到完整扫描 +// 入参: token 已写入的XML事件 +func (x *ofdXML) referenceToken(token xml.Token) { + if x.references == nil || x.err != nil { + return + } + s := x.references + switch value := token.(type) { + case xml.StartElement: + value.Name = xml.Name{Space: ofdNamespace, Local: strings.TrimPrefix(value.Name.Local, "ofd:")} + for _, attr := range value.Attr { + if editorResourceReferenceKind(attr.Name.Local, s.resource) != editorReferenceNone && !editorReferenceTextSafe(attr.Value) { + s.safe = false + } + } + token = value + case xml.EndElement: + value.Name = xml.Name{Space: ofdNamespace, Local: strings.TrimPrefix(value.Name.Local, "ofd:")} + token = value + case xml.CharData: + if s.capture && !editorReferenceTextSafe(string(value)) { + s.safe = false + } + } + if !s.accept(token) { + x.references = nil + } +} + +// referenceElement 收集已知叶节点的引用,其他结构仍完整扫描 +// 入参: name 元素名称, value 已编码的子树内容 +func (x *ofdXML) referenceElement(name string, value any) { + if x.references == nil || x.err != nil { + return + } + var attrs ofdAttrs + switch value := value.(type) { + case ShdColor: + attrs.add("ColorSpace", value.ColorSpace) + case *ShdColor: + if value == nil { + return + } + attrs.add("ColorSpace", value.ColorSpace) + case *Sound: + if value == nil { + return + } + attrs.add("ResourceID", value.ResourceID) + case *Movie: + if value == nil { + return + } + attrs.add("ResourceID", value.ResourceID) + case *GotoBookmark, *URI, *GotoA: + default: + x.references.safe = false + x.references = nil + return + } + x.referenceToken(xml.StartElement{Name: xml.Name{Local: "ofd:" + name}, Attr: attrs}) + x.referenceToken(xml.EndElement{Name: xml.Name{Local: "ofd:" + name}}) +} + +// editorReferenceTextSafe 判断XML编码是否保持引用文本的Unicode值 +// 入参: value 原始引用或字体用字 +// 返回: bool 是否无需字符替换 +func editorReferenceTextSafe(value string) bool { + if !utf8.ValidString(value) { + return false + } + for _, char := range value { + if char != '\t' && char != '\n' && char != '\r' && (char < 0x20 || char == 0xfffe || char == 0xffff) { + return false + } + } + return true +} diff --git a/ofdgo_editor_resources.go b/ofdgo_editor_resources.go index d051f7b..6d1d12a 100644 --- a/ofdgo_editor_resources.go +++ b/ofdgo_editor_resources.go @@ -27,6 +27,12 @@ import ( "strings" ) +const ( + editorReferenceNone = iota + editorReferenceID + editorReferenceFile +) + // editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活 type editorResourceRefs struct { ids map[string]bool @@ -41,6 +47,13 @@ type editorResourceRefs struct { // 入参: parts 已修改和新增的包内条目, progress 保存进度回调 // 返回: map[string]bool 可移除的二进制条目, error 读取错误 func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) { + return e.compactSourceReferences(parts, progress, nil) +} + +// compactSourceReferences 合并自产页面的已知引用,其他条目仍完整扫描 +// 入参: parts 输出条目, progress 保存进度回调, generated 自产页面及引用 +// 返回: map[string]bool 可移除条目, error 读取或取消错误 +func (e *Editor) compactSourceReferences(parts map[string][]byte, progress editorProgress, generated map[string]editorGeneratedReferences) (map[string]bool, error) { if e.source.reader.DocumentCount() > 1 || e.output != nil && e.output.protected { return nil, nil } @@ -68,9 +81,18 @@ func (e *Editor) compactSourceResources(parts map[string][]byte, progress editor if strings.HasSuffix(name, "/") { continue } + data, replaced := parts[name] + if known, ok := generated[name]; ok && replaced && known.refs != nil && known.matches(data) { + refs.merge(known.refs) + continue + } var input io.ReadCloser - if data, ok := parts[name]; ok { - input = io.NopCloser(bytes.NewReader(data)) + if replaced { + if known := generated[name]; data == nil && known.staged != nil { + input = known.staged.open() + } else { + input = io.NopCloser(bytes.NewReader(data)) + } } else { var err error input, err = reader.openFile(name) @@ -78,8 +100,14 @@ func (e *Editor) compactSourceResources(parts map[string][]byte, progress editor return nil, err } } + if e.output != nil { + input = imageInput{ReadCloser: input, context: e.output.ctx} + } resource, safe := refs.scan(input, name) input.Close() + if e.output != nil && e.output.ctx.Err() != nil { + return nil, e.output.ctx.Err() + } if !safe { return nil, nil } @@ -224,79 +252,16 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) { return false, !strings.EqualFold(path.Ext(name), ".xml") } decoder := xml.NewDecoder(buffer) - var stack []string - var fonts []*editorFontUsage - var text strings.Builder - resource, base := false, "" + scan := editorReferenceScan{refs: r, name: name, safe: true} for { token, err := decoder.Token() if err == io.EOF { - return resource, len(stack) == 0 + return scan.resource, len(scan.stack) == 0 } if err != nil { return false, false } - switch token := token.(type) { - case xml.StartElement: - if token.Name.Space != "" && token.Name.Space != ofdNamespace { - return false, false - } - if token.Name.Local == "CustomTags" || token.Name.Local == "Extensions" || token.Name.Local == "ExtendData" || token.Name.Local == "Data" { - return false, false - } - if len(stack) == 0 { - resource = token.Name.Local == "Res" - if resource { - for _, attr := range token.Attr { - if attr.Name.Local == "BaseLoc" { - base = attr.Value - } - } - } - } - var usage *editorFontUsage - if len(fonts) != 0 { - usage = fonts[len(fonts)-1] - if usage != nil && (stack[len(stack)-1] == "TextCode" || stack[len(stack)-1] == "Glyphs") { - usage.unsafe = true - } - } - for _, attr := range token.Attr { - if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" || attr.Name.Space == "http://www.w3.org/XML/1998/namespace" { - continue - } - if attr.Name.Space != "" { - return false, false - } - if attr.Name.Local != "ID" { - r.reference(name, base, attr.Name.Local, attr.Value, resource) - } - if attr.Name.Local == "Font" { - usage = r.fontUsage(attr.Value) - if token.Name.Local != "TextObject" && token.Name.Local != "Text" { - usage.unsafe = true - } - } else if attr.Name.Local == "Substitution" { - r.fontUsage(attr.Value).unsafe = true - } - } - stack = append(stack, token.Name.Local) - fonts = append(fonts, usage) - text.Reset() - case xml.CharData: - text.Write(token) - case xml.EndElement: - r.reference(name, base, token.Name.Local, text.String(), resource) - if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(text.String()) != "" { - r.fontUsage(text.String()).unsafe = true - } - if usage := fonts[len(fonts)-1]; usage != nil { - usage.text(token.Name.Local, text.String()) - } - stack = stack[:len(stack)-1] - fonts = fonts[:len(fonts)-1] - text.Reset() - case xml.Directive: + if !scan.accept(token) { return false, false } } @@ -305,32 +270,44 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) { // reference 记录标准资源标识和路径引用,不将普通数字或资源自身ID视为引用 // 入参: name 当前文件, base 资源目录, key 字段, value 值, resource 是否资源索引 func (r *editorResourceRefs) reference(name, base, key, value string, resource bool) { + kind := editorResourceReferenceKind(key, resource) + if kind == editorReferenceNone { + return + } value = strings.TrimSpace(value) if value == "" { return } - switch key { - case "Font", "ResourceID", "Substitution", "ImageMask", "Relative", "DrawParam", "ColorSpace", "DefaultCS", "Thumbnail", "TemplateID", "PageID", "PageRef", "RefId": + if kind == editorReferenceID { if id := editorResourceID(value); id != "" { r.ids[id] = true } return - case "DocRoot", "Signatures", "Cover", "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "FileLoc", "FileRef", "Profile", "SignedValue", "SchemaLoc", "ExtendData", "File": - case "BaseLoc": - if resource { - return - } - case "FontFile", "MediaFile": - if resource { - return - } - default: - return } r.files[editorResourceLocation(name, base, value)] = true r.files[strings.ToLower(cleanPackagePath(resolveResourcePath(name, base, value)))] = true } +// editorResourceReferenceKind 区分标准标识和文件引用,其他文本不参与资源扫描 +// 入参: key 字段名称, resource 是否资源索引 +// 返回: int 引用类型 +func editorResourceReferenceKind(key string, resource bool) int { + switch key { + case "Font", "ResourceID", "Substitution", "ImageMask", "Relative", "DrawParam", "ColorSpace", "DefaultCS", "Thumbnail", "TemplateID", "PageID", "PageRef", "RefId": + return editorReferenceID + case "DocRoot", "Signatures", "Cover", "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "FileLoc", "FileRef", "Profile", "SignedValue", "SchemaLoc", "ExtendData", "File": + return editorReferenceFile + case "BaseLoc": + case "FontFile", "MediaFile": + default: + return editorReferenceNone + } + if resource { + return editorReferenceNone + } + return editorReferenceFile +} + // editorResourceID 规范化ST_ID与ST_RefID的十进制表示 // 入参: value 标识文本 // 返回: string 标准十进制标识,无效时为空 diff --git a/ofdgo_editor_stage.go b/ofdgo_editor_stage.go new file mode 100644 index 0000000..d25ce8d --- /dev/null +++ b/ofdgo_editor_stage.go @@ -0,0 +1,147 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "archive/zip" + "bytes" + "compress/flate" + "encoding/xml" + "hash/crc32" + "io" + "unicode/utf8" +) + +// editorStagedPage 保存单次写出的压缩页面及校验值,不持有未压缩XML +type editorStagedPage struct { + data []byte + size uint64 + checksum uint32 +} + +// editorPageStager 复用单次保存的页面压缩器,不跨文档共享状态 +type editorPageStager struct { + compressor *flate.Writer +} + +// editorPageEncoding 同步统计编码字节及CRC,保留实际写入错误 +type editorPageEncoding struct { + writer io.Writer + size uint64 + checksum uint32 +} + +// Write 写入页面字节并统计实际接收的部分 +// 入参: data 未压缩编码数据 +// 返回: int 实际字节数, error 写入或短写错误 +func (w *editorPageEncoding) Write(data []byte) (int, error) { + n, err := w.writer.Write(data) + w.size += uint64(n) + w.checksum = crc32.Update(w.checksum, crc32.IEEETable, data[:n]) + if n != len(data) && err == nil { + err = io.ErrShortWrite + } + return n, err +} + +// encode 边编码边压缩自产页面,同步收集引用及用字 +// 入参: editor 保存快照, page 页面内容, references 本次引用扫描 +// 返回: *editorStagedPage 压缩页面, error 编码或取消错误 +func (s *editorPageStager) encode(editor *Editor, page PageContent, references *editorReferenceScan) (*editorStagedPage, error) { + var data bytes.Buffer + if s.compressor == nil { + level := flate.DefaultCompression + if editor.output != nil && editor.output.options.Mode != CompressionUnchanged { + level = flate.BestCompression + } + var err error + s.compressor, err = flate.NewWriter(&data, level) + if err != nil { + return nil, err + } + } else { + s.compressor.Reset(&data) + } + output := editorPageEncoding{writer: s.compressor} + x := &ofdXML{encoder: xml.NewEncoder(&output), references: references} + if editor.output != nil { + x.ctx = editor.output.ctx + } + x.page(page) + if err := x.finish(); err != nil { + return nil, err + } + if err := s.compressor.Close(); err != nil { + return nil, err + } + if !x.checkContext() { + return nil, x.err + } + return &editorStagedPage{data: data.Bytes(), size: output.size, checksum: output.checksum}, nil +} + +// open 流式读取暂存页面,仅用于无法完整收集编码引用的分支 +// 返回: io.ReadCloser 页面解压流 +func (p *editorStagedPage) open() io.ReadCloser { + return flate.NewReader(bytes.NewReader(p.data)) +} + +// writeStagedPage 直接写入已经压缩的自产页面,保持ZIP条目属性和取消语义 +// 入参: archive 输出归档, header 条目属性, page 暂存页面 +// 返回: error 写入、短写或取消错误 +func (e *Editor) writeStagedPage(archive *zip.Writer, header zip.FileHeader, page *editorStagedPage) error { + if e.output != nil && e.output.options.Mode != CompressionUnchanged { + if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil { + return err + } + } + header.Method, header.ReaderVersion = zip.Deflate, 20 + header.CreatorVersion = header.CreatorVersion&0xff00 | 20 + header.CRC32, header.UncompressedSize64, header.CompressedSize64 = page.checksum, page.size, uint64(len(page.data)) + header.Flags &^= 8 + if header.NonUTF8 { + header.Flags &^= 0x800 + } else if utf8.ValidString(header.Name) && utf8.ValidString(header.Comment) { + for _, value := range header.Name + header.Comment { + if value < 0x20 || value > 0x7d || value == 0x5c { + header.Flags |= 0x800 + break + } + } + } + entry, err := archive.CreateRaw(&header) + if err != nil { + return err + } + for offset := 0; offset < len(page.data); { + if e.output != nil { + if err := e.output.ctx.Err(); err != nil { + return err + } + } + end := min(offset+64<<10, len(page.data)) + if n, err := entry.Write(page.data[offset:end]); err != nil { + return err + } else if n != end-offset { + return io.ErrShortWrite + } + offset = end + } + if e.output != nil && e.output.options.Mode != CompressionUnchanged { + e.output.completed++ + return editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0) + } + return nil +} diff --git a/ofdgo_font_backend.go b/ofdgo_font_backend.go index 2f20d2c..29eea13 100644 --- a/ofdgo_font_backend.go +++ b/ofdgo_font_backend.go @@ -133,6 +133,12 @@ type resolvedFontResult struct { err error } +// fontDataKey 区分只读字体的起点和长度,指针保留源数据直到预算淘汰 +type fontDataKey struct { + data *byte + size int +} + // ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别 // 入参: id 字体资源标识, exact 是否要求精确匹配 // 返回: ResolvedFont 字体与来源, error 解析错误 @@ -165,7 +171,7 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) { prepared := &PreparedFont{ResolvedFont: resolved} definition := r.Reader.fontCache[id] if definition != nil && definition.FontFile != "" { - key := sha256.Sum256(resolved.Data) + key := r.fontDigest(resolved.Data) if content, ok := r.fontPreparations.get(key); ok { prepared.Data, prepared.Glyphs, prepared.CIDs, prepared.digest = content.Data, content.Glyphs, content.CIDs, content.digest r.preparedFonts[id] = prepared @@ -188,7 +194,7 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) { } } } - prepared.digest = sha256.Sum256(prepared.Data) + prepared.digest = r.fontDigest(prepared.Data) cost := min(len(prepared.Data), r.fontPreparations.limit)*2 + (len(prepared.Glyphs)+len(prepared.CIDs))*48 + 256 r.fontPreparations.put(key, PreparedFont{ResolvedFont: ResolvedFont{Data: prepared.Data}, Glyphs: prepared.Glyphs, CIDs: prepared.CIDs, digest: prepared.digest}, cost) } @@ -196,6 +202,22 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) { return prepared, nil } +// fontDigest 在预算内复用只读字体摘要,内容副本仍按摘要复用解析结果 +// 入参: data 不得修改的字体数据 +// 返回: [32]byte 字体摘要 +func (r *Renderer) fontDigest(data []byte) [32]byte { + if len(data) == 0 { + return sha256.Sum256(nil) + } + key := fontDataKey{data: &data[0], size: len(data)} + if digest, ok := r.fontDigests.get(key); ok { + return digest + } + digest := sha256.Sum256(data) + r.fontDigests.put(key, digest, min(cap(data), r.fontDigests.limit)+256) + return digest +} + // resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态 func (r *Renderer) resetFontCache() { r.fontSourcesCache = newFontSourceCache() @@ -206,6 +228,7 @@ func (r *Renderer) resetFontCache() { func (r *Renderer) resetFontBackendCache() { r.sharedBackendStates = make(map[any]any) r.preparedFonts = make(map[string]*PreparedFont) + r.fontDigests = &renderCache[fontDataKey, [32]byte]{limit: 4 << 20} r.fontPreparations = &renderCache[[32]byte, PreparedFont]{limit: 16 << 20} r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult) if r.fontSourcesCache != nil { diff --git a/ofdgo_font_cmap.go b/ofdgo_font_cmap.go index 9032589..e69a16b 100644 --- a/ofdgo_font_cmap.go +++ b/ofdgo_font_cmap.go @@ -19,6 +19,8 @@ import ( "encoding/binary" "fmt" "sort" + "unicode/utf16" + "unicode/utf8" ) // cmapSegment cmap表段结构 @@ -29,7 +31,7 @@ type cmapSegment struct { offset uint16 } -// normalizeCmap 转换旧式双字节子表,保留各编码记录及原字形编号 +// normalizeCmap 转换扩展字符子表,保留各编码记录及原字形编号 // 入参: data 字符映射表 // 返回: []byte 等价字符映射表, error 子表错误 func normalizeCmap(data []byte) ([]byte, error) { @@ -48,7 +50,8 @@ func normalizeCmap(data []byte) ([]byte, error) { if uint64(offset)+2 > uint64(len(data)) { return nil, fmt.Errorf("invalid cmap subtable offset") } - if binary.BigEndian.Uint16(data[offset:]) != 2 { + format := binary.BigEndian.Uint16(data[offset:]) + if format != 2 && format != 8 && format != 10 && format != 13 { continue } if out == nil { @@ -56,17 +59,35 @@ func normalizeCmap(data []byte) ([]byte, error) { } next, ok := converted[offset] if !ok { - mapping, err := parseCmapFormat2(data[offset:]) + var mapping map[rune]uint16 + var err error + language := uint32(0) + if format == 2 { + mapping, err = parseCmapFormat2(data[offset:]) + if err == nil { + language = uint32(binary.BigEndian.Uint16(data[offset+4:])) + } + } else { + mapping, err = parseCmapExtended(data[offset:]) + if err == nil { + language = binary.BigEndian.Uint32(data[offset+8:]) + } + } if err != nil { return nil, err } table := buildCmapTable(0, mapping) + if language > 65535 { + table = buildCmapTableFormat12(0, mapping) + } subtable := table[binary.BigEndian.Uint32(table[8:]):] - language := binary.BigEndian.Uint16(data[offset+4:]) if binary.BigEndian.Uint16(subtable) == 12 { - binary.BigEndian.PutUint32(subtable[8:], uint32(language)) + binary.BigEndian.PutUint32(subtable[8:], language) } else { - binary.BigEndian.PutUint16(subtable[4:], language) + binary.BigEndian.PutUint16(subtable[4:], uint16(language)) + } + if uint64(len(out))+uint64(len(subtable)) > 1<<32-1 { + return nil, fmt.Errorf("normalized cmap exceeds offset range") } next = uint32(len(out)) converted[offset] = next @@ -80,6 +101,110 @@ func normalizeCmap(data []byte) ([]byte, error) { return out, nil } +// parseCmapExtended 读取混合字符、扩展数组及常量分组,限制到有效Unicode范围 +// 入参: data 格式8、10或13子表 +// 返回: map[rune]uint16 独立字符映射, error 结构或映射错误 +func parseCmapExtended(data []byte) (map[rune]uint16, error) { + if len(data) < 12 { + return nil, fmt.Errorf("truncated extended cmap") + } + length := uint64(binary.BigEndian.Uint32(data[4:])) + if length < 12 || length > uint64(len(data)) { + return nil, fmt.Errorf("invalid extended cmap length") + } + data = data[:int(length)] + format := binary.BigEndian.Uint16(data) + result := make(map[rune]uint16) + add := func(code uint32, glyph uint16) error { + char := rune(code) + if code > utf8.MaxRune { + high, low := rune(code>>16), rune(code&65535) + if format != 8 || high < 0xd800 || high > 0xdbff || low < 0xdc00 || low > 0xdfff { + return fmt.Errorf("invalid extended cmap character") + } + char = utf16.DecodeRune(high, low) + } + if glyph != 0 && utf8.ValidRune(char) { + if previous, exists := result[char]; exists && previous != glyph { + return fmt.Errorf("ambiguous extended cmap character") + } + result[char] = glyph + } + return nil + } + if format == 10 { + if len(data) < 20 { + return nil, fmt.Errorf("truncated cmap format 10") + } + first, count := uint64(binary.BigEndian.Uint32(data[12:])), uint64(binary.BigEndian.Uint32(data[16:])) + if count > uint64((len(data)-20)/2) || first+count > utf8.MaxRune+1 { + return nil, fmt.Errorf("invalid cmap format 10 range") + } + for index := uint64(0); index < count; index++ { + if err := add(uint32(first+index), binary.BigEndian.Uint16(data[20+int(index)*2:])); err != nil { + return nil, err + } + } + return result, nil + } + offset := 16 + if format == 8 { + offset = 8208 + } else if format != 13 { + return nil, fmt.Errorf("invalid extended cmap format %d", format) + } + if len(data) < offset { + return nil, fmt.Errorf("truncated cmap format %d", format) + } + count := uint64(binary.BigEndian.Uint32(data[offset-4:])) + if count > uint64((len(data)-offset)/12) { + return nil, fmt.Errorf("invalid extended cmap group count") + } + var previous uint32 + for index := uint64(0); index < count; index++ { + group := data[offset+int(index)*12:] + first, last, glyph := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:]), uint64(binary.BigEndian.Uint32(group[8:])) + if first > last || index != 0 && first <= previous { + return nil, fmt.Errorf("invalid extended cmap group order") + } + previous = last + endGlyph := glyph + if format == 8 { + endGlyph += uint64(last) - uint64(first) + if first <= 65535 && last > 65535 { + return nil, fmt.Errorf("mixed cmap character range") + } + if last > utf8.MaxRune && (first>>16 != last>>16 || first>>16 < 0xd800 || last>>16 > 0xdbff || first&65535 < 0xdc00 || last&65535 > 0xdfff) { + return nil, fmt.Errorf("invalid UTF-16 cmap group") + } + } else if last > utf8.MaxRune { + return nil, fmt.Errorf("invalid cmap Unicode range") + } + if endGlyph > 65535 { + return nil, fmt.Errorf("extended cmap glyph index overflow") + } + for code := uint64(first); code <= uint64(last); code++ { + if format == 8 { + word := code + if code > 65535 { + word >>= 16 + } + if (data[12+word/8]&(1<<(7-word%8)) != 0) != (code > 65535) { + return nil, fmt.Errorf("invalid cmap character width") + } + } + id := glyph + if format == 8 { + id += code - uint64(first) + } + if err := add(uint32(code), uint16(id)); err != nil { + return nil, err + } + } + } + return result, nil +} + // parseCmapFormat2 解析混合单字节与双字节字符的子头映射 // 入参: data format 2子表 // 返回: map[rune]uint16 编码值到字形编号, error 子头或字形数组越界 @@ -155,8 +280,8 @@ func parseCmapMappings(data []byte) map[rune]uint16 { if platformID != 0 && platformID != 3 { continue } - offset := int(binary.BigEndian.Uint32(data[pos+4 : pos+8])) - if offset+2 > len(data) { + offset := uint64(binary.BigEndian.Uint32(data[pos+4 : pos+8])) + if offset+2 > uint64(len(data)) { continue } switch binary.BigEndian.Uint16(data[offset : offset+2]) { @@ -168,6 +293,19 @@ func parseCmapMappings(data []byte) map[rune]uint16 { parseCmapFormat6(data[offset:], result) case 12: parseCmapFormat12(data[offset:], result) + case 2, 8, 10, 13: + var mapping map[rune]uint16 + var err error + if binary.BigEndian.Uint16(data[offset:]) == 2 { + mapping, err = parseCmapFormat2(data[offset:]) + } else { + mapping, err = parseCmapExtended(data[offset:]) + } + if err == nil { + for char, glyph := range mapping { + result[char] = glyph + } + } } } if len(result) == 0 { @@ -263,27 +401,28 @@ 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) + length := uint64(binary.BigEndian.Uint32(data[4:8])) + if length < 16 || length > uint64(len(data)) { + return } - nGroups := int(binary.BigEndian.Uint32(data[12:16])) - for i := 0; i < nGroups; i++ { - pos := 16 + i*12 - if pos+12 > length { - break - } + nGroups := uint64(binary.BigEndian.Uint32(data[12:16])) + if nGroups > (length-16)/12 { + return + } + for i := uint64(0); i < nGroups; i++ { + pos := 16 + int(i)*12 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 { + if startChar > endChar || startGID > 65535 || startChar > utf8.MaxRune { + continue + } + last := min(uint64(endChar), uint64(startChar)+65535-uint64(startGID), uint64(utf8.MaxRune)) + for c := uint64(startChar); c <= last; c++ { + gid := uint64(startGID) + c - uint64(startChar) + if gid != 0 && utf8.ValidRune(rune(c)) { result[rune(c)] = uint16(gid) } - if c == endChar { - break - } } } } diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index 0926067..8d686ee 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -104,6 +104,13 @@ func (r *Reader) Fonts() ([]Font, error) { // 入参: id 字体资源标识 // 返回: []byte 字体数据, error 资源缺失或集合解析错误 func (r *Reader) FontData(id string) ([]byte, error) { + return r.fontData(id, false) +} + +// fontData 按调用方所有权读取内嵌字体,集合字体始终独立提取 +// 入参: id 字体资源标识, readonly 是否允许借用只读资源 +// 返回: []byte 字体数据, error 资源或集合错误 +func (r *Reader) fontData(id string, readonly bool) ([]byte, error) { if r.fontCache[id] == nil { if _, err := r.Fonts(); err != nil { return nil, err @@ -113,7 +120,11 @@ func (r *Reader) FontData(id string) ([]byte, error) { if f == nil || f.FontFile == "" { return nil, fmt.Errorf("embedded font %q not found", id) } - data, err := r.ResData(f.FontFile) + read := r.readFile + if readonly { + read = r.readFileView + } + data, err := read(r.ResPath(f.FontFile)) if err != nil { return nil, err } @@ -223,7 +234,7 @@ 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) + data, err := r.readFileView(name) if err != nil { return nil, err } diff --git a/ofdgo_font_source.go b/ofdgo_font_source.go index 94e9b56..428a3d4 100644 --- a/ofdgo_font_source.go +++ b/ofdgo_font_source.go @@ -84,6 +84,14 @@ 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)} } +// child 复用相同来源的文件索引与数据,匹配和回退状态保持独立 +// 返回: *fontSourceCache 独立文档缓存 +func (c *fontSourceCache) child() *fontSourceCache { + cache := newFontSourceCache() + cache.directories, cache.filesystems, cache.data = c.directories, c.filesystems, c.data + return cache +} + // readFontSource 读取并拆出独立字体,保留集合中的名称和样式选择 // 入参: source 字体来源, definition 字体定义 // 返回: []byte 字体数据, error 读取或集合错误 @@ -156,7 +164,7 @@ func (r *Renderer) resolveFontSource(backend FontBackend, id string, exact bool) definition = &Font{ID: id} } if definition.FontFile != "" { - data, err := r.Reader.FontData(id) + data, err := r.Reader.fontData(id, true) if err != nil { return ResolvedFont{}, err } diff --git a/ofdgo_geom_transform.go b/ofdgo_geom_transform.go index 5592684..706824a 100644 --- a/ofdgo_geom_transform.go +++ b/ofdgo_geom_transform.go @@ -42,6 +42,9 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) { return Point{x, y} } var result GeometryPath + if len(p) > 0 { + result = make(GeometryPath, 0, len(p)) + } var current, start Point for _, s := range p { originalSegment := s diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 37d4caa..c00b93d 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -21,7 +21,6 @@ import ( "math" "reflect" "strconv" - "strings" "github.com/xiaoqidun/pdfgo" ) @@ -62,6 +61,12 @@ type PDFImportReport struct { Warnings []pdfgo.Diagnostic } +// pdfPointBounds 逐点汇总边界,不保留文字或路径的临时坐标数组 +type pdfPointBounds struct { + minX, minY, maxX, maxY float64 + found bool +} + // pdfImporter 保存单次转换的对象与资源状态 type pdfImporter struct { ctx context.Context @@ -77,7 +82,7 @@ type pdfImporter struct { clipTexts map[*pdfgo.TextClip][]TextObject clipPaths *renderCache[[32]byte, pdfClipPath] cmykSpace string - objects []GraphicObject + objects *pdfObjectBuffer pages map[pdfgo.Reference]*pdfgo.Page pageIDs map[pdfgo.Reference]string pageIndexes map[pdfgo.Reference]int @@ -143,6 +148,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI editor := NewEditor() editor.SetRenderBackends(renderer.Backends()) editor.fontDirs, editor.fontFS = renderer.FontSources() + editor.fontSourcesCache = renderer.fontSourcesCache importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}} importer.report.Warnings = diagnostics importer.pageIndexes = make(map[pdfgo.Reference]int) @@ -270,18 +276,21 @@ func ConvertPDF(ctx context.Context, source io.ReaderAt, size int64, output io.W } return nil } - var written int64 - _, err = editor.WriteTo(convertWriter{context: ctx, writer: output, count: &written}) + _, err = editor.WriteToWithOptions(ctx, output, WriteOptions{}) return report, err } // commitObjects 将当前页面待转换对象写入编辑文档 // 返回: error 对象写入错误 func (p *pdfImporter) commitObjects() error { - if len(p.objects) == 0 { + if p.objects.len() == 0 { return nil } - if _, err := p.editor.CopyObjectsContext(p.ctx, p.page, p.objects, 0, 0); err != nil { + objects, err := p.objects.data(p.ctx) + if err != nil { + return err + } + if err := p.editor.appendOwnedObjects(p.ctx, p.page, objects); err != nil { return err } p.objects = nil @@ -313,11 +322,15 @@ func pdfPageMatrix(page *pdfgo.Page) (pdfgo.Matrix, float64, float64) { // 入参: values 数值列表 // 返回: string 空格分隔的数值 func pdfNumbers(values ...float64) string { - parts := make([]string, len(values)) + var buffer [128]byte + data := buffer[:0] for n, v := range values { - parts[n] = strconv.FormatFloat(v, 'g', -1, 64) + if n != 0 { + data = append(data, ' ') + } + data = strconv.AppendFloat(data, v, 'g', -1, 64) } - return strings.Join(parts, " ") + return string(data) } // pdfBoundary 序列化对象边界 @@ -329,17 +342,31 @@ func pdfBoundary(b Box) string { return pdfNumbers(b.X, b.Y, b.W, b.H) } // 入参: points 坐标点集合 // 返回: Box 包围框 func pdfBounds(points []pdfgo.Point) Box { - if len(points) == 0 { + var bounds pdfPointBounds + for _, point := range points { + bounds.add(point) + } + return bounds.box() +} + +// add 按原顺序累计一个控制点的最小和最大坐标 +// 入参: point 坐标点 +func (b *pdfPointBounds) add(point pdfgo.Point) { + if !b.found { + b.minX, b.minY, b.maxX, b.maxY = point.X, point.Y, point.X, point.Y + b.found = true + } + b.minX, b.minY = math.Min(b.minX, point.X), math.Min(b.minY, point.Y) + b.maxX, b.maxY = math.Max(b.maxX, point.X), math.Max(b.maxY, point.Y) +} + +// box 取得原精度边界,空集合仍返回零值 +// 返回: Box 坐标范围 +func (b *pdfPointBounds) box() Box { + if !b.found { return Box{} } - minX, minY, maxX, maxY := points[0].X, points[0].Y, points[0].X, points[0].Y - for _, p := range points { - minX = math.Min(minX, p.X) - minY = math.Min(minY, p.Y) - maxX = math.Max(maxX, p.X) - maxY = math.Max(maxY, p.Y) - } - return Box{minX, minY, math.Max(maxX-minX, 1e-6), math.Max(maxY-minY, 1e-6)} + return Box{b.minX, b.minY, math.Max(b.maxX-b.minX, 1e-6), math.Max(b.maxY-b.minY, 1e-6)} } // pathBoundsContext 逐段计算变换后的控制点边界,不创建临时点集合 @@ -462,12 +489,12 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) { if err := p.flushPath(); err != nil { return nil, err } - count := len(p.objects) + count := p.objects.len() textCount := p.report.TextObjects if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil { return nil, err } - p.objects = p.objects[:count] + p.objects.truncate(count) p.report.TextObjects = textCount objects = p.clipTexts[mark] } @@ -617,7 +644,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error { phase := mark.Style.DashPhase * scale object.DashOffset = &phase } - p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object}) + p.appendObject(GraphicObject{Type: "PathObject", PathObject: object}) p.report.PathObjects++ return nil } @@ -737,14 +764,18 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain if err := cell.flushPath(); err != nil { return nil, err } - pattern.CellContent.Objects = cell.objects + objects, err := cell.objects.data(p.ctx) + if err != nil { + return nil, err + } + pattern.CellContent.Objects = objects if width > xstep || height > ystep { columns, rows := math.Ceil(width/xstep), math.Ceil(height/ystep) if !finite(columns*rows) || columns*rows > float64(int(^uint(0)>>1)) { return nil, fmt.Errorf("PDF tiling pattern instance count overflow") } - bounds := make([]Box, len(cell.objects)) - for index, object := range cell.objects { + bounds := make([]Box, len(objects)) + for index, object := range objects { if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke { var err error bounds[index], err = ParseBox(object.PathObject.Boundary) @@ -753,14 +784,19 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain } } } - pattern.CellContent.Objects = nil + var expanded pdfObjectBuffer for row := 1 - int(rows); row <= 0; row++ { for column := 1 - int(columns); column <= 0; column++ { if err := p.ctx.Err(); err != nil { return nil, err } dx, dy := float64(column)*xstep*unit, float64(row)*ystep*unit - for index, object := range cell.objects { + for index, object := range objects { + if index%pdfObjectChunkLimit == 0 { + if err := p.ctx.Err(); err != nil { + return nil, err + } + } b := bounds[index] if b.W > 0 && b.H > 0 && (b.X+dx+b.W < 0 || b.Y+dy+b.H < 0 || b.X+dx > pattern.Width || b.Y+dy > pattern.Height) { continue @@ -769,10 +805,14 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain if err != nil { return nil, err } - pattern.CellContent.Objects = append(pattern.CellContent.Objects, translated) + expanded.append(translated) } } } + pattern.CellContent.Objects, err = expanded.data(p.ctx) + if err != nil { + return nil, err + } } return pattern, nil } diff --git a/ofdgo_pdf_annot.go b/ofdgo_pdf_annot.go index 517d72c..db9f740 100644 --- a/ofdgo_pdf_annot.go +++ b/ofdgo_pdf_annot.go @@ -98,7 +98,11 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page p.compositeNodes = stamp.compositeNodes p.transferBackdrop = stamp.transferBackdrop } - converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Remark: remark, Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}} + objects, err := stamp.objects.data(ctx) + if err != nil { + return err + } + converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Remark: remark, Appearance: Appearance{Boundary: pdfBoundary(box), Objects: objects}} if annotation.Subtype == "Redact" { converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Redact.Pending", Value: "true"}}} } diff --git a/ofdgo_pdf_buffer.go b/ofdgo_pdf_buffer.go new file mode 100644 index 0000000..01fbe53 --- /dev/null +++ b/ofdgo_pdf_buffer.go @@ -0,0 +1,119 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import "context" + +// pdfObjectChunkLimit 限制导入暂存片段的容量,不限制页面对象数量 +const pdfObjectChunkLimit = 256 + +// pdfObjectBuffer 分块收集导入对象,提交前只整理一次连续数据 +type pdfObjectBuffer struct { + parts [][]GraphicObject + size int +} + +// appendObject 追加本次导入对象,按需建立独立暂存缓冲 +// 入参: object 独立导入对象 +func (p *pdfImporter) appendObject(object GraphicObject) { + if p.objects == nil { + p.objects = &pdfObjectBuffer{} + } + p.objects.append(object) +} + +// append 追加对象,小批量连续存储,超过阈值后不复制前序片段 +// 入参: object 独立对象 +func (b *pdfObjectBuffer) append(object GraphicObject) { + index := len(b.parts) - 1 + if index == 0 && len(b.parts[0]) == cap(b.parts[0]) && cap(b.parts[0]) < pdfObjectChunkLimit { + part := b.parts[0] + next := make([]GraphicObject, len(part), min(pdfObjectChunkLimit, max(1, cap(part)*2))) + copy(next, part) + b.parts[0] = next + } + if index < 0 || len(b.parts[index]) == cap(b.parts[index]) { + capacity := pdfObjectChunkLimit + if index < 0 { + capacity = 1 + } + b.parts = append(b.parts, make([]GraphicObject, 0, capacity)) + index++ + } + b.parts[index] = append(b.parts[index], object) + b.size++ +} + +// len 获取暂存对象数量,空缓冲返回零 +// 返回: int 对象数量 +func (b *pdfObjectBuffer) len() int { + if b == nil { + return 0 + } + return b.size +} + +// truncate 回滚到已有对象位置,清理丢弃对象及片段引用 +// 入参: size 保留对象数量 +func (b *pdfObjectBuffer) truncate(size int) { + if size < 0 || size > b.len() { + panic("invalid PDF object buffer size") + } + if size == b.len() { + return + } + remaining := size + for index, part := range b.parts { + if remaining <= len(part) { + clear(part[remaining:]) + b.parts[index] = part[:remaining] + clear(b.parts[index+1:]) + b.parts = b.parts[:index+1] + b.size = size + return + } + remaining -= len(part) + } +} + +// data 按绘制顺序整理独立对象数组,取消时保留原片段供重试 +// 入参: ctx 取消上下文 +// 返回: []GraphicObject 暂存数据, error 取消错误 +func (b *pdfObjectBuffer) data(ctx context.Context) ([]GraphicObject, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if b.len() == 0 { + return nil, nil + } + if len(b.parts) == 1 { + return b.parts[0][:b.size:b.size], nil + } + objects := make([]GraphicObject, b.size) + offset := 0 + for _, part := range b.parts { + if err := ctx.Err(); err != nil { + return nil, err + } + offset += copy(objects[offset:], part) + } + if err := ctx.Err(); err != nil { + return nil, err + } + b.parts[0] = objects + clear(b.parts[1:]) + b.parts = b.parts[:1] + return objects, nil +} diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index c17f878..9f58937 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -50,6 +50,7 @@ type pdfCompositor struct { width, height int inverse pdfgo.Matrix cache *pdfCompositeCache + scratch *pdfCompositeScratch masks map[*pdfgo.SoftMask][]float64 meshes map[pdfMeshKey][]pdfShadingPixel gradients map[pdfGradientKey][]pdfShadingPixel @@ -71,6 +72,15 @@ type pdfCompositeGeometry struct { box Box } +// pdfCompositeColorSampler 在单次绘制内复用完全相同参数的轴向颜色,不近似函数 +type pdfCompositeColorSampler struct { + paint pdfgo.Paint + space *pdfgo.ColorSpace + intent pdfgo.Name + conversion pdfgo.ColorConversion + values map[float64][4]float64 +} + // pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容 type pdfCompositeCache struct { geometry map[pdfCompositeKey]pdfCompositeGeometry @@ -242,8 +252,17 @@ func (p *pdfImporter) compositeTextBounds(mark pdfgo.TextMark, stroke bool) Box // 返回: []pdfCompositeNode 原始图元, error 解析错误 func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) { active := make(map[*pdfgo.Font]map[string]bool) + var seenFonts map[*pdfgo.Font]bool var externalNames map[string]bool var embeddedFonts map[*pdfgo.Font]bool + var textMarks []pdfgo.TextMark + storeText := func(mark pdfgo.TextMark) *pdfgo.TextMark { + if len(textMarks) == cap(textMarks) { + textMarks = make([]pdfgo.TextMark, 0, min(64, max(8, cap(textMarks)*2))) + } + textMarks = append(textMarks, mark) + return &textMarks[len(textMarks)-1] + } var collect func(func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) collect = func(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) { var nodes []pdfCompositeNode @@ -259,7 +278,11 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p return nil } v.Text = func(mark pdfgo.TextMark) error { - if mark.Font.Subtype != "Type3" { + if mark.Font.Subtype != "Type3" && !seenFonts[mark.Font] { + if seenFonts == nil { + seenFonts = make(map[*pdfgo.Font]bool) + } + seenFonts[mark.Font] = true if len(mark.Font.Program) == 0 { if externalNames == nil { externalNames = make(map[string]bool) @@ -277,7 +300,13 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p embeddedFonts[mark.Font] = true } } - var text []pdfCompositeNode + text := &nodes + if mark.Object != nil && mark.Object.Knockout { + if len(nodes) == 0 || nodes[len(nodes)-1].textObject != mark.Object { + nodes = append(nodes, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, Knockout: true}, textObject: mark.Object}) + } + text = &nodes[len(nodes)-1].children + } if mark.Font.Subtype != "Type3" { if err := p.halftone(mark.Style); err != nil { return err @@ -289,10 +318,10 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p for i := range mark.Glyphs { glyph := mark glyph.Glyphs, glyph.Positions, glyph.Clip = mark.Glyphs[i:i+1], mark.Positions[i:i+1], nil - text = append(text, pdfCompositeNode{text: &glyph}) + *text = append(*text, pdfCompositeNode{text: storeText(glyph)}) } } else { - text = append(text, pdfCompositeNode{text: &mark}) + *text = append(*text, pdfCompositeNode{text: storeText(mark)}) } } else { glyphs := active[mark.Font] @@ -305,25 +334,17 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name) } glyphs[glyph.Name] = true - children, err := collect(func(visitor pdfgo.Visitor) error { return p.reader.WalkType3Glyph(p.ctx, mark, index, visitor) }) + source := mark + children, err := collect(func(visitor pdfgo.Visitor) error { return p.reader.WalkType3Glyph(p.ctx, source, index, visitor) }) delete(glyphs, glyph.Name) if err != nil { return err } part := mark part.Glyphs, part.Positions = mark.Glyphs[index:index+1], mark.Positions[index:index+1] - text = append(text, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1}, children: children, glyph: &part}) + *text = append(*text, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1}, children: children, glyph: &part}) } } - if mark.Object != nil && mark.Object.Knockout { - if len(nodes) == 0 || nodes[len(nodes)-1].textObject != mark.Object { - nodes = append(nodes, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, Knockout: true}, textObject: mark.Object}) - } - node := &nodes[len(nodes)-1] - node.children = append(node.children, text...) - } else { - nodes = append(nodes, text...) - } return nil } v.Image = func(mark pdfgo.ImageMark) error { @@ -624,6 +645,9 @@ func (n pdfCompositeNode) coverage(p *pdfImporter, stroke bool) error { // 入参: backdrop 前序图元, node 当前效果, space 页面混合空间, flatten 是否合并页面背景 // 返回: error 合成或保存错误 func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompositeNode, space *pdfgo.ColorSpace, flatten bool) error { + if err := p.ctx.Err(); err != nil { + return err + } scene, box, err := p.compileGroup(func(local *pdfImporter) error { if node.group != nil { return node.coverage(local, false) @@ -650,21 +674,28 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo if err != nil { return err } + if err := p.ctx.Err(); err != nil { + return err + } output := image.NewNRGBA(image.Rect(0, 0, w, h)) cache := p.compositingCache() step := 25.4 / p.rasterDPI - buffer := make([]pdfCompositePixel, min(256, w)*min(256, h)) + buffer := make([]pdfCompositePixel, min(pdfCompositeTileSize, w)*min(pdfCompositeTileSize, h)) + scratch := &pdfCompositeScratch{} var transfers []*pdfgo.TransferFunction var previousTransfers []*pdfgo.TransferFunction if pdfCompositeHasTransfer(backdrop) || pdfCompositeHasTransfer([]pdfCompositeNode{node}) { transfers = make([]*pdfgo.TransferFunction, len(buffer)) previousTransfers = make([]*pdfgo.TransferFunction, len(buffer)) } - for y := 0; y < h; y += 256 { - for x := 0; x < w; x += 256 { + for y := 0; y < h; y += pdfCompositeTileSize { + for x := 0; x < w; x += pdfCompositeTileSize { + if err := p.ctx.Err(); err != nil { + return err + } runtime.Gosched() - width, height := min(256, w-x), min(256, h-y) - c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}} + width, height := min(pdfCompositeTileSize, w-x), min(pdfCompositeTileSize, h-y) + c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, scratch: scratch, masks: map[*pdfgo.SoftMask][]float64{}} if transfers != nil { c.transfers = transfers[:width*height] clear(c.transfers) @@ -682,6 +713,11 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo return err } for i, pixel := range pixels { + if i%pdfCompositeTileSize == 0 { + if err := p.ctx.Err(); err != nil { + return err + } + } if pixel.effect == 0 && (c.transfers == nil || c.transfers[i] == previousTransfers[i]) { continue } @@ -711,6 +747,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo } output.SetNRGBA(x+i%width, y+i/width, color.NRGBA{R: uint8(math.Round(rgb[0] * 255)), G: uint8(math.Round(rgb[1] * 255)), B: uint8(math.Round(rgb[2] * 255)), A: uint8(math.Round(alpha * 255))}) } + c.releaseMasks() } } return p.appendRasterGroup(output, box, 1) diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index f49fdf8..c77f796 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -15,6 +15,7 @@ package ofdgo import ( + "context" "fmt" "math" @@ -25,6 +26,9 @@ import ( // 入参: nodes 图元, pixels 背景与输出像素, space 混合空间 // 返回: error 几何或颜色错误 func (c *pdfCompositor) draw(nodes []pdfCompositeNode, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error { + if err := c.importer.ctx.Err(); err != nil { + return err + } for _, node := range nodes { if err := c.importer.ctx.Err(); err != nil { return err @@ -59,9 +63,13 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix result := pixels grouped := fill && stroke && (style.FillOverprint || style.StrokeOverprint) && style.Fill.Alpha == style.Stroke.Alpha if fill && stroke { - initial = append([]pdfCompositePixel(nil), pixels...) + initial = c.acquirePixels(len(pixels)) + defer c.releasePixels(initial) + copy(initial, pixels) if grouped { - result = append([]pdfCompositePixel(nil), pixels...) + result = c.acquirePixels(len(pixels)) + defer c.releasePixels(result) + copy(result, pixels) for i := range result { result[i].effect = 0 result[i].shape = 0 @@ -88,7 +96,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix } } if grouped { - return pdfCompositeGroup(pixels, initial, result, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion) + return pdfCompositeGroup(c.importer.ctx, pixels, initial, result, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion) } return nil } @@ -146,6 +154,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf if err != nil { return err } + defer c.releasePixels(pattern) } var shading []pdfShadingPixel if paint.Mesh != nil && (node.image == nil || node.image.Image.ImageMask) { @@ -161,6 +170,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf } var uniformValues [4]float64 uniformReady := false + var sampler *pdfCompositeColorSampler + if g := paint.Axial; node.image == nil && shading == nil && g != nil && g.Bounds == nil && g.Background == nil && + (g.Start.Y == g.End.Y && c.inverse[2] == 0 || g.Start.X == g.End.X && c.inverse[1] == 0) { + sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion} + } geometry, err := c.geometry(node, outline) if err != nil { return err @@ -231,7 +245,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf opacity *= alpha } else { var visible bool - values, visible, err = pdfCompositeColor(paint, point, space, style.RenderingIntent, style.ColorConversion) + if sampler != nil { + values, visible, err = sampler.color(point) + } else { + values, visible, err = pdfCompositeColor(paint, point, space, style.RenderingIntent, style.ColorConversion) + } if !visible { shape = 0 } @@ -311,11 +329,19 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi if !g.Isolated && !g.Knockout && g.Alpha == 1 && g.SoftMask == nil && pdfNormalBlend(g.BlendMode) && space.Equal(parent) { return c.draw(node.children, pixels, space) } - initial := make([]pdfCompositePixel, len(pixels)) - if !g.Isolated { + initial := c.acquirePixels(len(pixels)) + defer c.releasePixels(initial) + if g.Isolated { + clear(initial) + } else { sameSpace := space.Equal(parent) components := parent.Components() for i, pixel := range pixels { + if i%pdfCompositeTileSize == 0 { + if err := c.importer.ctx.Err(); err != nil { + return err + } + } values := pixel.values if !sameSpace { var err error @@ -327,7 +353,9 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi initial[i] = pdfCompositePixel{values: values, alpha: pixel.alpha} } } - result := append([]pdfCompositePixel(nil), initial...) + result := c.acquirePixels(len(initial)) + defer c.releasePixels(result) + copy(result, initial) if g.Knockout { if err := c.drawKnockout(node.children, initial, result, space); err != nil { return err @@ -343,20 +371,30 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi } if g.AlphaIsShape { for i := range result { + if i%pdfCompositeTileSize == 0 { + if err := c.importer.ctx.Err(); err != nil { + return err + } + } result[i].shape *= g.Alpha if mask != nil { result[i].shape *= mask[i] } } } - return pdfCompositeGroup(pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion) + return pdfCompositeGroup(c.importer.ctx, pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion) } // drawKnockout 将每个对象与组初始背景合成,再按对象形状替换先前对象 // 入参: nodes 组图元, initial 初始背景, pixels 组输出, space 混合空间 // 返回: error 图元或合成错误 func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error { - candidate := make([]pdfCompositePixel, len(initial)) + if err := c.importer.ctx.Err(); err != nil { + return err + } + candidate := c.acquirePixels(len(initial)) + defer c.releasePixels(candidate) + clear(candidate) for _, node := range nodes { var box Box var err error @@ -373,6 +411,9 @@ func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels [ continue } for y := region.Min.Y; y < region.Max.Y; y++ { + if err := c.importer.ctx.Err(); err != nil { + return err + } start, end := y*c.width+region.Min.X, y*c.width+region.Max.X copy(candidate[start:end], initial[start:end]) } @@ -407,9 +448,12 @@ func pdfCompositeKnockout(target *pdfCompositePixel, initial, source pdfComposit } // pdfCompositeGroup 移除组初始背景贡献后,将组结果合成到父空间 -// 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数 +// 入参: ctx 取消上下文, pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数 // 返回: error 颜色或混合错误 -func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error { +func pdfCompositeGroup(ctx context.Context, pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error { + if err := ctx.Err(); err != nil { + return err + } if len(pixels) != len(initial) || len(pixels) != len(result) || mask != nil && len(mask) != len(pixels) { return fmt.Errorf("invalid PDF transparency group buffers") } @@ -420,6 +464,11 @@ func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, paren } components, parentComponents := space.Components(), parent.Components() for i, pixel := range result { + if i%pdfCompositeTileSize == 0 { + if err := ctx.Err(); err != nil { + return err + } + } alpha := opacity if mask != nil { alpha *= mask[i] @@ -441,6 +490,9 @@ func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, paren // 入参: mask 蒙版或空值, inherited 未指定蒙版空间时继承的空间 // 返回: []float64 蒙版透明度或空值, error 蒙版错误 func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) ([]float64, error) { + if err := c.importer.ctx.Err(); err != nil { + return nil, err + } if mask == nil { return nil, nil } @@ -450,7 +502,7 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) nodes, ok := c.cache.masks[mask] var err error if !ok { - nodes, err = c.importer.collectCompositeNodes(mask.Walk) + nodes, err = c.importer.collectCompositeNodes(func(v pdfgo.Visitor) error { return mask.WalkContext(c.importer.ctx, v) }) if err != nil { return nil, err } @@ -460,9 +512,16 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) if space == nil { space = inherited } - pixels := make([]pdfCompositePixel, c.width*c.height) + pixels := c.acquirePixels(c.width * c.height) + defer c.releasePixels(pixels) + clear(pixels) if mask.Subtype == "Luminosity" { for i := range pixels { + if i%pdfCompositeTileSize == 0 { + if err := c.importer.ctx.Err(); err != nil { + return nil, err + } + } copy(pixels[i].values[:], mask.Backdrop) pixels[i].alpha = 1 } @@ -472,8 +531,19 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) if err := local.draw(nodes, pixels, space); err != nil { return nil, err } - result := make([]float64, len(pixels)) + result := c.scratch.masks.acquire(len(pixels)) + complete := false + defer func() { + if !complete { + c.scratch.masks.release(result) + } + }() for i, pixel := range pixels { + if i%pdfCompositeTileSize == 0 { + if err := c.importer.ctx.Err(); err != nil { + return nil, err + } + } v := pixel.alpha if mask.Subtype == "Luminosity" { v, err = space.Luminosity(pixel.values[:space.Components()], "RelativeColorimetric") @@ -486,6 +556,7 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) return nil, err } } + complete = true c.masks[mask] = result return result, nil } @@ -494,15 +565,6 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) // 入参: mark 图像, point 页面坐标, space 混合空间 // 返回: [4]float64 源颜色, float64 图像透明度, error 解码或变换错误 func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, space *pdfgo.ColorSpace) ([4]float64, float64, error) { - source := c.cache.images[mark.Image] - if source == nil { - var err error - source, err = mark.Image.DecodeComponentsContext(c.importer.ctx) - if err != nil { - return [4]float64{}, 0, err - } - c.cache.images[mark.Image] = source - } inverse, found := c.cache.imageMatrix[mark] if !found { var ok bool @@ -519,6 +581,15 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa if point.X < 0 || point.X > 1 || point.Y < 0 || point.Y > 1 { return [4]float64{}, 0, nil } + source := c.cache.images[mark.Image] + if source == nil { + var err error + source, err = mark.Image.DecodeComponentsViewContext(c.importer.ctx) + if err != nil { + return [4]float64{}, 0, err + } + c.cache.images[mark.Image] = source + } x, y := point.X*float64(source.Rect.Dx())-.5, (1-point.Y)*float64(source.Rect.Dy())-.5 _, nativeProcess := source.Process.CMYKMask() process := mark.Style.FillOverprint && space.Model == "DeviceCMYK" && !space.Calibrated() && len(source.Colorants) > 0 @@ -724,6 +795,31 @@ func pdfCompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorS return values, true, err } +// color 复用精确参数对应的颜色,区域、背景及其他画刷保持原求值路径 +// 入参: point 页面坐标 +// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误 +func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, error) { + g := s.paint.Axial + if g == nil || g.Bounds != nil || g.Background != nil || s.paint.Function != nil || s.paint.Tiling != nil { + return pdfCompositeColor(s.paint, point, s.space, s.intent, s.conversion) + } + position, visible := pdfShadingPosition(s.paint, point) + if !visible { + return [4]float64{}, false, nil + } + if values, ok := s.values[position]; ok { + return values, true, nil + } + values, visible, err := pdfCompositeColor(s.paint, point, s.space, s.intent, s.conversion) + if err == nil && visible && len(s.values) < 2*pdfCompositeTileSize { + if s.values == nil { + s.values = make(map[float64][4]float64) + } + s.values[position] = values + } + return values, visible, err +} + // pdfCompositePaintOver 以兼容叠印计算零色料分量,再应用对象混合模式 // 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式, compatible 是否保留零色料背景, marked 原生过程通道,nil使用普通空间 // 返回: error 未支持的混合模式 diff --git a/ofdgo_pdf_composite_pattern.go b/ofdgo_pdf_composite_pattern.go index 75b8c39..3e61506 100644 --- a/ofdgo_pdf_composite_pattern.go +++ b/ofdgo_pdf_composite_pattern.go @@ -57,11 +57,12 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, return nil, fmt.Errorf("PDF tiling pattern instance range overflow") } } - result := make([]pdfCompositePixel, len(backdrop)) + result := c.acquirePixels(len(backdrop)) for i, pixel := range backdrop { result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha} } - candidate := make([]pdfCompositePixel, len(backdrop)) + candidate := c.acquirePixels(len(backdrop)) + defer c.releasePixels(candidate) disjoint := source.BBox.XMax-source.BBox.XMin <= xstep && source.BBox.YMax-source.BBox.YMin <= ystep pixelStep := 25.4 / c.importer.rasterDPI localInverse, _ := pattern.local.matrix.Inverse() @@ -92,10 +93,11 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, } } cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X + float64(left)*pixelStep - dx, Y: c.box.Y + float64(top)*pixelStep - dy, W: float64(width) * pixelStep, H: float64(height) * pixelStep}, - width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), masks: map[*pdfgo.SoftMask][]float64{}} + width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), scratch: c.scratch, masks: map[*pdfgo.SoftMask][]float64{}} if err := cell.draw(pattern.nodes, pixels, space); err != nil { return nil, err } + cell.releaseMasks() for y := range height { for x := range width { index := (top+y)*c.width + left + x @@ -105,6 +107,11 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, } } for i := range result { + if i%pdfCompositeTileSize == 0 { + if err := c.importer.ctx.Err(); err != nil { + return nil, err + } + } pixel := &result[i] if pixel.effect != 0 { for j := 0; j < space.Components(); j++ { @@ -187,7 +194,7 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co source := p.compositingCache().images[node.image.Image] if source == nil { var err error - source, err = node.image.Image.DecodeComponentsContext(p.ctx) + source, err = node.image.Image.DecodeComponentsViewContext(p.ctx) if err != nil { return false, err } diff --git a/ofdgo_pdf_composite_scratch.go b/ofdgo_pdf_composite_scratch.go new file mode 100644 index 0000000..ca16ca5 --- /dev/null +++ b/ofdgo_pdf_composite_scratch.go @@ -0,0 +1,87 @@ +// 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 + +// pdfCompositeTileSize 限定单次合成块的像素边长 +const pdfCompositeTileSize = 256 + +// pdfCompositeBuffers 在一次局部合成中保留至多八个块容量内的空闲缓冲 +type pdfCompositeBuffers[T any] [8][]T + +// pdfCompositeScratch 复用像素及蒙版缓冲,不跨页持有活动内容 +type pdfCompositeScratch struct { + pixels pdfCompositeBuffers[pdfCompositePixel] + masks pdfCompositeBuffers[float64] +} + +// acquire 借用尚未初始化的独立缓冲,嵌套调用不复用活动缓冲 +// 入参: count 所需元素数量 +// 返回: []T 可由当前调用独占的缓冲 +func (s *pdfCompositeBuffers[T]) acquire(count int) []T { + selected := -1 + for i, buffer := range s { + if cap(buffer) >= count && (selected < 0 || cap(buffer) < cap(s[selected])) { + selected = i + } + } + if selected < 0 { + return make([]T, count) + } + buffer := s[selected] + s[selected] = nil + return buffer[:count] +} + +// release 归还已不再访问的缓冲,超出块容量或空闲槽数量时直接释放 +// 入参: buffer 当前调用借用的缓冲 +func (s *pdfCompositeBuffers[T]) release(buffer []T) { + if len(buffer) == 0 || cap(buffer) > pdfCompositeTileSize*pdfCompositeTileSize { + return + } + for i, stored := range s { + if stored == nil { + s[i] = buffer + return + } + } +} + +// acquirePixels 为当前合成器借用独立像素缓冲 +// 入参: count 所需像素数量 +// 返回: []pdfCompositePixel 尚未初始化的独占缓冲 +func (c *pdfCompositor) acquirePixels(count int) []pdfCompositePixel { + if c.scratch == nil { + c.scratch = &pdfCompositeScratch{} + } + return c.scratch.pixels.acquire(count) +} + +// releasePixels 归还当前调用已不再访问的像素缓冲 +// 入参: pixels 当前调用借用的缓冲 +func (c *pdfCompositor) releasePixels(pixels []pdfCompositePixel) { + if c.scratch != nil { + c.scratch.pixels.release(pixels) + } +} + +// releaseMasks 在当前块绘制结束后归还蒙版结果,不复用其他块的坐标缓存 +func (c *pdfCompositor) releaseMasks() { + if c.scratch != nil { + for _, mask := range c.masks { + c.scratch.masks.release(mask) + } + } + clear(c.masks) +} diff --git a/ofdgo_pdf_font.go b/ofdgo_pdf_font.go index 5878db2..14394a8 100644 --- a/ofdgo_pdf_font.go +++ b/ofdgo_pdf_font.go @@ -58,7 +58,7 @@ func (p *pdfImporter) embeddedTextFont(source *pdfgo.Font, glyphs []pdfgo.Glyph) return nil } for _, glyph := range glyphs { - if utf8.RuneCountInString(glyph.Text) != 1 { + if glyph.Name == ".notdef" || utf8.RuneCountInString(glyph.Text) != 1 { return nil } char, _ := utf8.DecodeRuneInString(glyph.Text) diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index d7a717d..0ff7239 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -297,7 +297,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) return err } object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: p.color(region.paint), Clips: objectClips} - p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object}) + p.appendObject(GraphicObject{Type: "PathObject", PathObject: object}) p.report.PathObjects++ } return nil @@ -358,6 +358,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, editor.SetRenderBackends(p.editor.Backends()) editor.SetFontDirs(p.editor.fontDirs...) editor.SetFontFS(p.editor.fontFS...) + editor.fontSourcesCache = p.renderer.fontSourcesCache if _, err := editor.AddPage(p.pageWidth, p.pageHeight); err != nil { return nil, box, err } @@ -377,7 +378,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, if err := local.flushPath(); err != nil { return nil, box, err } - if len(local.objects) == 0 { + if local.objects.len() == 0 { return nil, box, nil } if err := local.commitObjects(); err != nil { @@ -391,17 +392,23 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, page := &editor.pages[0] renderer := p.renderer.childRenderer(reader) renderer.TransparentBackground = true - for _, layer := range page.Content.Layer { - for _, object := range layer.Objects { - if err := p.ctx.Err(); err != nil { - return nil, box, err - } - bounds, err := renderer.ObjectBounds(object, layer.DrawParam) - if err != nil { - return nil, box, err - } - box = unionTextBox(box, bounds) - } + scene, err := renderer.CompilePage(page) + if canceled := p.ctx.Err(); canceled != nil { + return nil, box, canceled + } + if err != nil { + return nil, box, err + } + if scene == nil { + return nil, box, fmt.Errorf("PDF local compositing: invalid compiled scene") + } + geometry, err := renderer.Geometry() + if err != nil { + return nil, box, err + } + box, err = scene.BoundsContext(p.ctx, geometry) + if err != nil { + return nil, Box{}, err } if box.W <= 0 || box.H <= 0 { return nil, box, nil @@ -413,11 +420,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, if box.W <= 0 || box.H <= 0 { return nil, box, nil } - scene, err := renderer.CompilePage(page) - if canceled := p.ctx.Err(); canceled != nil { - return nil, box, canceled - } - return scene, box, err + return scene, box, nil } // renderGroupScene 在指定区域渲染已编译场景,不修改缓存的命令与裁剪 @@ -466,7 +469,7 @@ func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float6 } alpha := int(math.Round(opacity * 255)) object := ImageObject{ResourceID: id, Boundary: pdfBoundary(box), CTM: pdfNumbers(box.W, 0, 0, box.H, 0, 0), Alpha: &alpha} - p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: object}) + p.appendObject(GraphicObject{Type: "ImageObject", ImageObject: object}) p.report.ImageObjects++ if !p.rasterWarned { p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF effect locally composited at %g DPI; other objects retained", p.rasterDPI)}) @@ -659,7 +662,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) { region, err = geometry.Combine(region, shape, operation) return err } - if err := mask.Walk(visitor); err != nil { + if err := mask.WalkContext(p.ctx, visitor); err != nil { return pdfgo.Path{}, err } path := pdfgo.Path{} diff --git a/ofdgo_pdf_mask.go b/ofdgo_pdf_mask.go index 0cec9a7..88e6b60 100644 --- a/ofdgo_pdf_mask.go +++ b/ofdgo_pdf_mask.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "image" - "image/color" "math" "github.com/xiaoqidun/pdfgo" @@ -36,6 +35,9 @@ func (p *pdfImporter) rasterMaskAllowed(err error) bool { // 入参: source 源图像, box 页面区域, mask PDF软蒙版 // 返回: image.Image 合成图像, error 蒙版解释或渲染错误 func (p *pdfImporter) applySoftMask(source image.Image, box Box, mask *pdfgo.SoftMask) (image.Image, error) { + if err := p.ctx.Err(); err != nil { + return nil, err + } inverse, ok := p.matrix.Inverse() if !ok { return nil, fmt.Errorf("invalid PDF mask transform") @@ -44,20 +46,31 @@ func (p *pdfImporter) applySoftMask(source image.Image, box Box, mask *pdfgo.Sof w, h := out.Rect.Dx(), out.Rect.Dy() step := 25.4 / p.rasterDPI cache := p.compositingCache() - for y := 0; y < h; y += 256 { - for x := 0; x < w; x += 256 { - width, height := min(256, w-x), min(256, h-y) - c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}} - alpha, err := c.mask(mask, &pdfgo.ColorSpace{Model: "DeviceRGB"}) + scratch := &pdfCompositeScratch{} + space := &pdfgo.ColorSpace{Model: "DeviceRGB"} + for y := 0; y < h; y += pdfCompositeTileSize { + for x := 0; x < w; x += pdfCompositeTileSize { + if err := p.ctx.Err(); err != nil { + return nil, err + } + width, height := min(pdfCompositeTileSize, w-x), min(pdfCompositeTileSize, h-y) + c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, scratch: scratch, masks: map[*pdfgo.SoftMask][]float64{}} + alpha, err := c.mask(mask, space) if err != nil { return nil, err } for i, opacity := range alpha { + if i%width == 0 { + if err := p.ctx.Err(); err != nil { + return nil, err + } + } px, py := out.Rect.Min.X+x+i%width, out.Rect.Min.Y+y+i/width - pixel := color.NRGBAModel.Convert(source.At(px, py)).(color.NRGBA) + pixel := imageNRGBAAt(source, px, py) pixel.A = uint8(math.Round(float64(pixel.A) * opacity)) out.SetNRGBA(px, py, pixel) } + c.releaseMasks() } } return out, nil diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index f2307f5..9ec7ea7 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -28,10 +28,11 @@ import ( "github.com/xiaoqidun/pdfgo" ) -// pdfImageKey 区分同一图像流在不同渲染意图下的资源 +// pdfImageKey 区分同一图像流的渲染意图及模板填充色 type pdfImageKey struct { stream *pdfgo.Stream intent pdfgo.Name + tint color.NRGBA64 } // pdfImageResource 保存图像的有效颜色空间及对应资源,避免跨资源重映射复用 @@ -84,6 +85,9 @@ func (s *pdfStencilImage) NRGBA64At(x, y int) color.NRGBA64 { // 入参: mark PDF图像绘制信息 // 返回: error 错误信息 func (p *pdfImporter) image(mark pdfgo.ImageMark) error { + if err := p.ctx.Err(); err != nil { + return err + } if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" { return &pdfgo.UnsupportedError{Feature: "image blend mode " + string(mark.Style.BlendMode)} } @@ -111,14 +115,28 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { } source := mark.Image key := pdfImageKey{stream: source.Stream, intent: source.Intent} - id := "" - if !source.ImageMask { - for _, resource := range p.imageIDs[key] { - if reflect.DeepEqual(resource.space, source.ColorSpace) { - id = resource.id - break + if source.ImageMask { + fill := mark.Style.Fill.RGB + if values := mark.Style.Fill.CMYK; values != nil { + space := &pdfgo.ColorSpace{Model: "DeviceCMYK"} + fill, err = space.RGB(values[:], mark.Style.RenderingIntent) + if err != nil { + return err } } + key.tint = color.NRGBA64{ + R: uint16(math.Round(fill[0] * 65535)), + G: uint16(math.Round(fill[1] * 65535)), + B: uint16(math.Round(fill[2] * 65535)), + A: 65535, + } + } + id := "" + for _, resource := range p.imageIDs[key] { + if reflect.DeepEqual(resource.space, source.ColorSpace) { + id = resource.id + break + } } if id == "" { jbig2Original, err := source.JBIG2FileContext(p.ctx) @@ -137,41 +155,27 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { } } if source.ImageMask { - fill := mark.Style.Fill.RGB - if values := mark.Style.Fill.CMYK; values != nil { - space := &pdfgo.ColorSpace{Model: "DeviceCMYK"} - fill, err = space.RGB(values[:], mark.Style.RenderingIntent) - if err != nil { - return err - } - } - tint := color.NRGBA64{ - R: uint16(math.Round(fill[0] * 65535)), - G: uint16(math.Round(fill[1] * 65535)), - B: uint16(math.Round(fill[2] * 65535)), - } - decoded = &pdfStencilImage{source: decoded, fill: tint} + decoded = &pdfStencilImage{source: decoded, fill: key.tint} } - var encoded bytes.Buffer - if len(jbig2Original) != 0 { - encoded.Write(jbig2Original) - } else if len(jpegOriginal) != 0 { - encoded.Write(jpegOriginal) - } else { + data := jbig2Original + if len(data) == 0 { + data = jpegOriginal + } + if len(data) == 0 { + var encoded bytes.Buffer if err := pdfgo.EncodePNG(p.ctx, &encoded, decoded); err != nil { return err } + data = encoded.Bytes() } - id, err = p.editor.AddImage(encoded.Bytes()) + id, err = p.editor.AddImage(data) if err != nil { return err } - if !source.ImageMask { - if p.imageIDs == nil { - p.imageIDs = map[pdfImageKey][]pdfImageResource{} - } - p.imageIDs[key] = append(p.imageIDs[key], pdfImageResource{space: source.ColorSpace, id: id}) + if p.imageIDs == nil { + p.imageIDs = map[pdfImageKey][]pdfImageResource{} } + p.imageIDs[key] = append(p.imageIDs[key], pdfImageResource{space: source.ColorSpace, id: id}) } return p.appendImage(mark, id) } @@ -188,7 +192,7 @@ func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error { return err } object := ImageObject{Boundary: pdfBoundary(box), ResourceID: id, CTM: pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y), Alpha: &alpha, Clips: clips} - p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: object}) + p.appendObject(GraphicObject{Type: "ImageObject", ImageObject: object}) p.report.ImageObjects++ return nil } @@ -240,7 +244,8 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { if mark.Clip == nil && p.rasterMaskAllowed(err) { mask := mark.Style.SoftMask mark.Style.SoftMask = nil - return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Text(mark) }) + masked := mark + return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Text(masked) }) } return err } @@ -249,7 +254,8 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { return &pdfgo.UnsupportedError{Feature: "text color separation overprint"} } if p.warning != nil && ((paintMode == 0 || paintMode == 2) && pdfGradientError(mark.Style.Fill) != nil || (paintMode == 1 || paintMode == 2) && pdfGradientError(mark.Style.Stroke) != nil) { - return p.compositeRegion(nil, pdfCompositeNode{text: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false) + gradient := mark + return p.compositeRegion(nil, pdfCompositeNode{text: &gradient}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false) } font := mark.Font embedded := len(font.Program) != 0 @@ -311,11 +317,18 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { outline = metrics.(FontOutlines) } fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3 - if stroke && !embedded { + needOutline := stroke + for _, glyph := range mark.Glyphs { + needOutline = needOutline || glyph.Name == ".notdef" + } + if needOutline && !embedded { if resolved, err := p.editor.editorFont(id); err == nil { if provider, ok := resolved.(FontOutlines); ok { metrics, outline = resolved, provider for _, glyph := range mark.Glyphs { + if glyph.Name == ".notdef" { + continue + } char, _ := utf8.DecodeRuneInString(glyph.Text) if utf8.RuneCountInString(glyph.Text) != 1 || resolved.GlyphIndex(char) == 0 && char != 0 { metrics, outline = nil, nil @@ -329,7 +342,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { var glyphPaths []pdfgo.Path const unit = 25.4 / 72 m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, 0, 0}) - points := make([]pdfgo.Point, 0, 4*len(mark.Glyphs)) + var extent pdfPointBounds object := TextObject{Font: id, Size: mark.Size * unit, HScale: mark.HorizontalScale, TextCode: make([]TextCode, 0, len(mark.Glyphs))} if embedded { object.CGTransform = make([]CGTransform, 0, len(mark.Glyphs)) @@ -340,16 +353,16 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { return err } gid := glyph.ID + undefined := glyph.Name == ".notdef" + if !embedded && undefined { + gid = 0 + } if embedded && !reused && font.ProgramType == "FontFile" { gid = imported.type1Glyphs[glyph.Name] glyph.HasID = true } - if glyph.Text == "" && glyph.HasID { - character := getUnicodeFromName(glyph.Name) - if character == 0 { - character = packedGlyphRune(gid) - } - glyph.Text = string(character) + if glyph.Text == "" && (glyph.HasID || undefined) { + glyph.Text = "\u00a4" } if embedded && !glyph.HasID { if utf8.RuneCountInString(glyph.Text) != 1 { @@ -367,18 +380,18 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { if !embedded && glyph.Text == "" { return &pdfgo.UnsupportedError{Feature: "unmapped external font character"} } - if outlineStroke && !embedded { + if outlineStroke && !embedded && !undefined { char, _ := utf8.DecodeRuneInString(glyph.Text) gid = metrics.GlyphIndex(char) } origin := mark.Positions[n] object.TextCode = append(object.TextCode, TextCode{X: pdfNumbers(origin.X * unit), Y: pdfNumbers(-origin.Y * unit), Value: glyph.Text}) - if embedded { - count := utf8.RuneCountInString(glyph.Text) + count := utf8.RuneCountInString(glyph.Text) + if embedded || undefined { object.CGTransform = append(object.CGTransform, CGTransform{CodePosition: position, CodeCount: count, GlyphCount: 1, Glyphs: strconv.Itoa(int(gid))}) - position += count } - if embedded || outlineStroke { + position += count + if embedded || outlineStroke || undefined && outline != nil { var path GeometryPath var bounds Box var err error @@ -408,7 +421,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } } for _, point := range []pdfgo.Point{{X: bounds.X, Y: bounds.Y}, {X: bounds.X + bounds.W, Y: bounds.Y}, {X: bounds.X, Y: bounds.Y + bounds.H}, {X: bounds.X + bounds.W, Y: bounds.Y + bounds.H}} { - points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X*mark.HorizontalScale, Y: -origin.Y*unit + point.Y})) + extent.add(m.Apply(pdfgo.Point{X: origin.X*unit + point.X*mark.HorizontalScale, Y: -origin.Y*unit + point.Y})) } if outlineStroke { matrix := mark.Matrix.Mul(pdfgo.Matrix{mark.HorizontalScale / unit, 0, 0, -1 / unit, origin.X, origin.Y}) @@ -420,12 +433,22 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } } else { width := glyph.Width / 1000 * object.Size * mark.HorizontalScale - for _, point := range []pdfgo.Point{{X: 0, Y: -object.Size}, {X: width, Y: -object.Size}, {X: 0, Y: object.Size / 4}, {X: width, Y: object.Size / 4}} { - points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X, Y: -origin.Y*unit + point.Y})) + bounds := Box{X: 0, Y: -object.Size, W: width, H: object.Size * 1.25} + if undefined { + b := font.BoundingBox + if b == nil { + b = font.FallbackBoundingBox + } + if b != nil { + bounds = Box{X: b.XMin / 1000 * object.Size * mark.HorizontalScale, Y: -b.YMax / 1000 * object.Size, W: (b.XMax - b.XMin) / 1000 * object.Size * mark.HorizontalScale, H: (b.YMax - b.YMin) / 1000 * object.Size} + } + } + for _, point := range []pdfgo.Point{{X: bounds.X, Y: bounds.Y}, {X: bounds.X + bounds.W, Y: bounds.Y}, {X: bounds.X, Y: bounds.Y + bounds.H}, {X: bounds.X + bounds.W, Y: bounds.Y + bounds.H}} { + extent.add(m.Apply(pdfgo.Point{X: origin.X*unit + point.X, Y: -origin.Y*unit + point.Y})) } } } - box := pdfBounds(points) + box := extent.box() if stroke && !outlineStroke { strokeMatrix := mark.StrokeMatrix if strokeMatrix == (pdfgo.Matrix{}) { @@ -508,7 +531,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { object.Visible = new(bool) object.Fill, object.Stroke = new(bool), new(bool) } - p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object}) + p.appendObject(GraphicObject{Type: "TextObject", TextObject: object}) p.report.TextObjects++ return nil } diff --git a/ofdgo_pdf_text_group.go b/ofdgo_pdf_text_group.go index 31e9aa8..f2ad716 100644 --- a/ofdgo_pdf_text_group.go +++ b/ofdgo_pdf_text_group.go @@ -68,21 +68,10 @@ func (p *pdfImporter) disjointText(node pdfCompositeNode) (bool, error) { } var box Box for _, command := range scene.Commands { - path := geometryFromRaster(command.Path) - if command.Image != nil { - b := command.Image.Bounds() - path = geometryRectangle(Box{X: float64(b.Min.X), Y: float64(b.Min.Y), W: float64(b.Dx()), H: float64(b.Dy())}) - } - path, err = geometry.Transform(path, MatrixFromValues([6]float64(command.Transform))) + path, err := rasterCommandGeometry(geometry, command) if err != nil { return false, err } - if command.Stroke != nil { - path, err = geometry.Stroke(path, *command.Stroke) - if err != nil { - return false, err - } - } path, err = geometry.Transform(path, MatrixFromValues([6]float64(inverse))) if err != nil { return false, err diff --git a/ofdgo_raster.go b/ofdgo_raster.go index 76b0dc3..ad6cb5c 100644 --- a/ofdgo_raster.go +++ b/ofdgo_raster.go @@ -102,14 +102,17 @@ type RasterSpread struct { Period float64 } -// PixelSize 检查页面尺寸并计算目标像素大小 +// PixelSize 检查页面尺寸并计算目标像素大小,合法正尺寸至少保留一个像素 // 返回: int 宽度, int 高度, error 无效尺寸 func (p *RasterPage) PixelSize() (int, int, error) { if p == nil || !rasterPositive(p.Width) || !rasterPositive(p.Height) || !rasterPositive(p.DPI) { return 0, 0, fmt.Errorf("invalid raster page size or DPI") } scale := p.DPI / 25.4 - w, h := math.Floor(p.Width*scale+0.5), math.Floor(p.Height*scale+0.5) + if !rasterPositive(scale) { + return 0, 0, fmt.Errorf("invalid raster page size or DPI") + } + w, h := math.Max(1, math.Floor(p.Width*scale+0.5)), math.Max(1, math.Floor(p.Height*scale+0.5)) if !rasterPositive(w) || !rasterPositive(h) || w*h > math.MaxInt32/4 { return 0, 0, fmt.Errorf("invalid raster pixel dimensions: %g x %g", w, h) } diff --git a/ofdgo_raster_bounds.go b/ofdgo_raster_bounds.go new file mode 100644 index 0000000..df03508 --- /dev/null +++ b/ofdgo_raster_bounds.go @@ -0,0 +1,121 @@ +// 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" + "fmt" +) + +// Bounds 度量已编译指令的页面坐标范围,不重新定位文字或读取图片像素 +// 包含描边与指令裁剪,不应用页面边界,不排除图片透明像素或渐变透明区域 +// 入参: geometry 几何后端 +// 返回: Box 毫米坐标范围, error 几何或指令错误 +func (p *RasterPage) Bounds(geometry GeometryBackend) (Box, error) { + return p.BoundsContext(context.Background(), geometry) +} + +// BoundsContext 按上下文度量只读绘制指令,取消或失败时不返回部分范围 +// 入参: ctx 取消上下文, geometry 几何后端 +// 返回: Box 毫米坐标范围, error 取消、几何或指令错误 +func (p *RasterPage) BoundsContext(ctx context.Context, geometry GeometryBackend) (Box, error) { + if err := ctx.Err(); err != nil { + return Box{}, err + } + if p == nil { + return Box{}, fmt.Errorf("invalid raster page") + } + if geometry == nil { + return Box{}, fmt.Errorf("raster bounds: %w", ErrBackendUnavailable) + } + var box Box + for index, command := range p.Commands { + if err := ctx.Err(); err != nil { + return Box{}, err + } + path, err := rasterCommandGeometry(geometry, command) + if err == nil { + var bounds Box + bounds, err = geometry.Bounds(path) + box = unionTextBox(box, bounds) + } + if err != nil { + return Box{}, fmt.Errorf("raster command %d bounds: %w", index, err) + } + } + if err := ctx.Err(); err != nil { + return Box{}, err + } + return box, nil +} + +// rasterCommandGeometry 共用指令的页面变换、描边与裁剪,不修改路径或样式 +// 入参: geometry 几何后端, command 绘制指令 +// 返回: GeometryPath 页面轮廓, error 几何或指令错误 +func rasterCommandGeometry(geometry GeometryBackend, command RasterCommand) (GeometryPath, error) { + if command.Clip != nil && len(command.Clip) == 0 { + return nil, nil + } + var path GeometryPath + if command.Image != nil { + bounds := command.Image.Bounds() + if bounds.Empty() { + return nil, nil + } + path = geometryRectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())}) + } else { + if len(command.Path) == 0 || command.Paint.Gradient == nil && command.Paint.Color.A == 0 { + return nil, nil + } + for _, segment := range command.Path { + if segment.Verb > RasterClose { + return nil, fmt.Errorf("unsupported raster path verb %d", segment.Verb) + } + } + path = geometryFromRaster(command.Path) + } + matrix := MatrixFromValues([6]float64(command.Transform)) + path, err := geometry.Transform(path, matrix) + if err != nil { + return nil, err + } + if command.Image == nil && command.Stroke != nil { + if err := validateStroke(*command.Stroke); err != nil { + return nil, err + } + path, err = geometry.Stroke(path, *command.Stroke) + if err != nil { + return nil, err + } + } else if command.EvenOdd && command.Clip != nil { + path, err = geometry.Normalize(path, true) + if err != nil { + return nil, err + } + } + if command.Clip != nil { + for _, segment := range command.Clip { + if segment.Verb > RasterClose { + return nil, fmt.Errorf("unsupported raster clip verb %d", segment.Verb) + } + } + clip := geometryFromRaster(command.Clip) + path, err = clipGeometry(geometry, path, &clip) + if err != nil { + return nil, err + } + } + return path, nil +} diff --git a/ofdgo_reader.go b/ofdgo_reader.go index 0c290cf..a6eb8c5 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -524,20 +524,40 @@ func validPackagePath(name string) bool { // 入参: name 文件名 // 返回: []byte 文件内容, error 错误信息 func (r *Reader) readFile(name string) ([]byte, error) { + data, borrowed, err := r.readFileData(name) + if borrowed { + data = bytes.Clone(data) + } + return data, err +} + +// readFileView 借用已加载文件或读取压缩文件,返回数据不得修改 +// 入参: name 文件名 +// 返回: []byte 只读文件内容, error 错误信息 +func (r *Reader) readFileView(name string) ([]byte, error) { + data, _, err := r.readFileData(name) + return data, err +} + +// readFileData 统一解析包内路径并区分缓存借用与独立读取 +// 入参: name 文件名 +// 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息 +func (r *Reader) readFileData(name string) ([]byte, bool, 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 + return data, true, nil } if f, ok := r.packageFile(name); ok { if data, ok := r.files[cleanPackagePath(f.Name)]; ok { - return bytes.Clone(data), nil + return data, true, nil } - return readZipFile(f) + data, err := readZipFile(f) + return data, false, err } - return nil, fmt.Errorf("file not found: %s", name) + return nil, false, fmt.Errorf("file not found: %s", name) } // openFile 打开文档内的文件流 diff --git a/ofdgo_render_text.go b/ofdgo_render_text.go index 3c25e84..ccb2f29 100644 --- a/ofdgo_render_text.go +++ b/ofdgo_render_text.go @@ -15,7 +15,6 @@ package ofdgo import ( - "crypto/sha256" "fmt" "math" "strings" @@ -262,7 +261,7 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position // 返回: FontMetrics 字体度量, error 字体解析错误 func (r *Renderer) preparedMetrics(prepared *PreparedFont) (FontMetrics, error) { if prepared.digest == ([32]byte{}) { - prepared.digest = sha256.Sum256(prepared.Data) + prepared.digest = r.fontDigest(prepared.Data) } key := prepared.digest if metrics, ok := r.fontMetrics.get(key); ok { diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index e4138ec..321d7e7 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -49,6 +49,7 @@ type Renderer struct { sharedBackendStates map[any]any resolvedFonts map[resolvedFontKey]resolvedFontResult preparedFonts map[string]*PreparedFont + fontDigests *renderCache[fontDataKey, [32]byte] fontPreparations *renderCache[[32]byte, PreparedFont] fontSourcesCache *fontSourceCache fontMetrics *renderCache[[32]byte, FontMetrics] @@ -84,7 +85,7 @@ func (r *Renderer) FontSources() ([]string, []fs.FS) { return append([]string(nil), r.fontDirs...), append([]fs.FS(nil), r.fontFS...) } -// childRenderer 创建继承当前配置的子渲染器 +// childRenderer 继承配置并复用字体文件索引与数据,文档匹配结果保持独立 // 入参: reader 子阅读器 // 返回: *Renderer 子渲染器 func (r *Renderer) childRenderer(reader *Reader) *Renderer { @@ -102,7 +103,9 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer { renderer := NewRenderer(reader, opts...) renderer.sharedBackendStates = r.sharedBackendStates renderer.fontPreparations = r.fontPreparations + renderer.fontDigests = r.fontDigests renderer.fontMetrics = r.fontMetrics + renderer.fontSourcesCache = r.fontSourcesCache.child() renderer.decodeImages = r.decodeImages renderer.TransparentBackground = r.TransparentBackground return renderer diff --git a/ofdgo_renderer_font_loader_canvas.go b/ofdgo_renderer_font_loader_canvas.go index 576d1b5..7cce56e 100644 --- a/ofdgo_renderer_font_loader_canvas.go +++ b/ofdgo_renderer_font_loader_canvas.go @@ -14,11 +14,7 @@ package ofdgo -import ( - "crypto/sha256" - - "github.com/tdewolff/canvas" -) +import "github.com/tdewolff/canvas" // loadFont 加载字体 // 入参: fontID 字体ID @@ -42,7 +38,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { fontData := resolved.Data fontStyle := canvasFontStyle(of) if resolved.digest == ([32]byte{}) { - resolved.digest = sha256.Sum256(fontData) + resolved.digest = r.fontDigest(fontData) } key := canvasFontKey{digest: resolved.digest, name: of.FontName, style: fontStyle} cache := r.canvasFontFamilies() diff --git a/ofdgo_renderer_geom_canvas.go b/ofdgo_renderer_geom_canvas.go index 60e944c..59537f2 100644 --- a/ofdgo_renderer_geom_canvas.go +++ b/ofdgo_renderer_geom_canvas.go @@ -67,6 +67,16 @@ func (CanvasBackend) Bounds(path GeometryPath) (Box, error) { // 入参: path 页面路径, matrix 页面变换 // 返回: GeometryPath 新路径, error 无效路径错误 func (CanvasBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath, error) { + curves := true + for _, segment := range path { + if segment.Verb == GeometryArc { + curves = false + break + } + } + if curves { + return path.Transform(matrix) + } p, err := geometryToCanvasPath(&path) if err != nil { return nil, err diff --git a/ofdgo_text_shape.go b/ofdgo_text_shape.go index 4073cfb..1bdcc82 100644 --- a/ofdgo_text_shape.go +++ b/ofdgo_text_shape.go @@ -282,7 +282,7 @@ func shapedLineWidth(runes []rune, advances []float64, spacing float64) float64 // validateTextGlyphs 校验显式字形编号及原文区间,不允许跨TextCode或重叠映射 // 入参: obj 文字对象, glyphCount 字体字形数量 // 返回: error 无效映射 -func validateTextGlyphs(obj TextObject, glyphCount uint16) error { +func validateTextGlyphs(obj TextObject, glyphCount int) error { if len(obj.CGTransform) == 0 { return nil } diff --git a/ofdgo_writer.go b/ofdgo_writer.go index e629c3c..facca85 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -17,6 +17,7 @@ package ofdgo import ( "archive/zip" "bytes" + "context" "encoding/hex" "encoding/xml" "fmt" @@ -43,8 +44,13 @@ type ofdCountingWriter struct { // ofdXML 使用标准ofd命名空间写出XML type ofdXML struct { - encoder *xml.Encoder - err error + encoder *xml.Encoder + references *editorReferenceScan + ctx context.Context + attrs ofdAttrs + escaped bytes.Buffer + spaces bytes.Buffer + err error } // ofdAttrs OFD节点属性 @@ -316,7 +322,7 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro } fonts, images, spaces, definitions := e.usedResources() writeXML := func(name string, encode func(*ofdXML)) error { - data, err := encodeOFDXML(encode) + data, err := e.encodeXML(encode) if err != nil { return err } @@ -538,6 +544,9 @@ func (x *ofdXML) page(page PageContent) { x.end("Area") x.start("Content", nil) for _, layer := range page.Content.Layer { + if x.err != nil { + return + } x.layer(layer) } x.end("Content") @@ -554,6 +563,9 @@ func (x *ofdXML) layer(layer Layer) { attrs.add("DrawParam", layer.DrawParam) x.start("Layer", attrs) for _, object := range layer.Objects { + if x.err != nil { + return + } x.object(object, false) } x.end("Layer") @@ -566,6 +578,9 @@ func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace) if len(spaces) != 0 { x.start("ColorSpaces", nil) for _, space := range spaces { + if x.err != nil { + return + } x.start("ColorSpace", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: space.ID}, {Name: xml.Name{Local: "Type"}, Value: space.Type}, {Name: xml.Name{Local: "BitsPerComponent"}, Value: strconv.Itoa(space.BitsPerComponent)}}) x.end("ColorSpace") } @@ -574,6 +589,9 @@ func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace) if len(fonts) != 0 { x.start("Fonts", nil) for _, resource := range fonts { + if x.err != nil { + return + } font := resource.font var attrs ofdAttrs attrs.add("ID", font.ID) @@ -600,6 +618,9 @@ func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace) if len(images) != 0 { x.start("MultiMedias", nil) for _, resource := range images { + if x.err != nil { + return + } image := resource.image var attrs ofdAttrs attrs.add("ID", image.ID) @@ -625,11 +646,21 @@ func newOFDXML(writer io.Writer) *ofdXML { // token 写入XML标记并保留首个错误 // 入参: token XML标记 func (x *ofdXML) token(token xml.Token) { - if x.err == nil { + if x.checkContext() { x.err = x.encoder.EncodeToken(token) + x.referenceToken(token) } } +// checkContext 保留首个编码或取消错误 +// 返回: bool 是否可以继续编码 +func (x *ofdXML) checkContext() bool { + if x.err == nil && x.ctx != nil { + x.err = x.ctx.Err() + } + return x.err == nil +} + // root 写入声明命名空间的根节点 // 入参: name 节点名, attrs 属性 func (x *ofdXML) root(name string, attrs ofdAttrs) { @@ -660,7 +691,7 @@ func (x *ofdXML) text(name, value string) { // finish 完成XML输出 // 返回: error 错误信息 func (x *ofdXML) finish() error { - if x.err != nil { + if !x.checkContext() { return x.err } return x.encoder.Close() @@ -702,8 +733,30 @@ func (a *ofdAttrs) alpha(value *int) { // 入参: encode 节点写入方法 // 返回: []byte XML数据, error 错误信息 func encodeOFDXML(encode func(*ofdXML)) ([]byte, error) { + return encodeOFDXMLContext(nil, encode) +} + +// encodeXML 按本次保存上下文编码,不改变普通快照的输出 +// 入参: encode 节点写入方法 +// 返回: []byte XML数据, error 编码或取消错误 +func (e *Editor) encodeXML(encode func(*ofdXML)) ([]byte, error) { + var ctx context.Context + if e.output != nil { + ctx = e.output.ctx + } + return encodeOFDXMLContext(ctx, encode) +} + +// encodeOFDXMLContext 编码文档节点,失败时不返回部分数据 +// 入参: ctx 取消上下文,可为nil, encode 节点写入方法 +// 返回: []byte XML数据, error 编码或取消错误 +func encodeOFDXMLContext(ctx context.Context, encode func(*ofdXML)) ([]byte, error) { var data bytes.Buffer x := newOFDXML(&data) + x.ctx = ctx + if !x.checkContext() { + return nil, x.err + } encode(x) if err := x.finish(); err != nil { return nil, err @@ -714,7 +767,10 @@ func encodeOFDXML(encode func(*ofdXML)) ([]byte, error) { // object 按OFD规定顺序写出基本对象 // 入参: object 图形对象, root 是否为根节点 func (x *ofdXML) object(object GraphicObject, root bool) { - var attrs ofdAttrs + if !x.checkContext() { + return + } + attrs := x.attrs[:0] var fill, stroke *FillColor var actions []Action if root { @@ -799,6 +855,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) { attrs.alpha(obj.Alpha) actions = obj.Actions } + x.attrs = attrs x.start(object.Type, attrs) x.actions(actions) if object.Type == "ImageObject" { @@ -817,24 +874,31 @@ func (x *ofdXML) object(object GraphicObject, root bool) { x.color("StrokeColor", stroke) offset, index := 0, 0 for _, code := range object.TextObject.TextCode { + if x.err != nil { + return + } end := offset + len(textCodeRunes(code.Value)) for index < len(object.TextObject.CGTransform) && object.TextObject.CGTransform[index].CodePosition < end { - transform := object.TextObject.CGTransform[index] - attrs := ofdAttrs{ - {Name: xml.Name{Local: "CodePosition"}, Value: strconv.Itoa(transform.CodePosition - offset)}, - {Name: xml.Name{Local: "CodeCount"}, Value: strconv.Itoa(transform.CodeCount)}, - {Name: xml.Name{Local: "GlyphCount"}, Value: strconv.Itoa(transform.GlyphCount)}, + if x.err != nil { + return } + transform := object.TextObject.CGTransform[index] + attrs := x.attrs[:0] + attrs.add("CodePosition", strconv.Itoa(transform.CodePosition-offset)) + attrs.add("CodeCount", strconv.Itoa(transform.CodeCount)) + attrs.add("GlyphCount", strconv.Itoa(transform.GlyphCount)) + x.attrs = attrs x.start("CGTransform", attrs) x.text("Glyphs", transform.Glyphs) x.end("CGTransform") index++ } - var attrs ofdAttrs + attrs := x.attrs[:0] attrs.add("X", code.X) attrs.add("Y", code.Y) attrs.add("DeltaX", code.DeltaX) attrs.add("DeltaY", code.DeltaY) + x.attrs = attrs x.textCode(code.Value, attrs) offset = end } @@ -845,23 +909,35 @@ func (x *ofdXML) object(object GraphicObject, root bool) { // clips 写出对象裁剪,保留区域内的路径与文字 // 入参: clips 裁剪集合,nil不输出节点 func (x *ofdXML) clips(clips *Clips) { - if clips == nil { + if clips == nil || !x.checkContext() { return } var attrs ofdAttrs attrs.flag("TransFlag", clips.TransFlag) x.start("Clips", attrs) for _, clip := range clips.Clip { + if x.err != nil { + return + } x.start("Clip", nil) for _, area := range clip.Area { + if x.err != nil { + return + } var attrs ofdAttrs attrs.add("CTM", area.CTM) attrs.add("DrawParam", area.DrawParam) x.start("Area", attrs) for _, path := range area.Path { + if x.err != nil { + return + } x.object(GraphicObject{Type: "Path", PathObject: path}, false) } for _, text := range area.Text { + if x.err != nil { + return + } x.object(GraphicObject{Type: "Text", TextObject: text}, false) } x.end("Area") @@ -874,23 +950,41 @@ func (x *ofdXML) clips(clips *Clips) { // textCode 使用XML字符引用转义空格,避免依赖阅读器保留原始空白 // 入参: value OFD文字内容, attrs 文字定位属性 func (x *ofdXML) textCode(value string, attrs ofdAttrs) { - if x.err != nil { + if !x.checkContext() { return } - var escaped bytes.Buffer - if x.err = xml.EscapeText(&escaped, []byte(value)); x.err != nil { + x.escaped.Reset() + if x.err = xml.EscapeText(&x.escaped, []byte(value)); x.err != nil { return } + data := x.escaped.Bytes() + if next := bytes.IndexByte(data, ' '); next >= 0 { + x.spaces.Reset() + for replaced := 0; next >= 0; replaced++ { + if replaced&255 == 0 && !x.checkContext() { + return + } + x.spaces.Write(data[:next]) + x.spaces.WriteString(" ") + data = data[next+1:] + next = bytes.IndexByte(data, ' ') + } + x.spaces.Write(data) + data = x.spaces.Bytes() + } content := struct { Text []byte `xml:",innerxml"` - }{Text: bytes.ReplaceAll(escaped.Bytes(), []byte(" "), []byte(" "))} + }{Text: data} x.err = x.encoder.EncodeElement(content, xml.StartElement{Name: xml.Name{Local: "ofd:TextCode"}, Attr: attrs}) + x.referenceToken(xml.StartElement{Name: xml.Name{Local: "ofd:TextCode"}, Attr: attrs}) + x.referenceToken(xml.CharData(value)) + x.referenceToken(xml.EndElement{Name: xml.Name{Local: "ofd:TextCode"}}) } // color 写出纯色、渐变和图案,保留颜色空间与透明度 // 入参: name 节点名, color 颜色 func (x *ofdXML) color(name string, color *FillColor) { - if color == nil { + if color == nil || !x.checkContext() { return } var attrs ofdAttrs @@ -940,6 +1034,9 @@ func (x *ofdXML) meshShading(name string, extend, columns int, points []ShdPoint } x.start(name, attrs) for _, point := range points { + if x.err != nil { + return + } var attrs ofdAttrs attrs.add("X", ofdNumber(point.X)) attrs.add("Y", ofdNumber(point.Y)) @@ -974,6 +1071,9 @@ func shadingAttrs(mapType string, mapUnit float64, extend, start, end string) of func (x *ofdXML) shading(name string, attrs ofdAttrs, segments []ShdSegment) { x.start(name, attrs) for _, segment := range segments { + if x.err != nil { + return + } var attrs ofdAttrs if !segment.positionMissing { attrs.add("Position", ofdNumber(segment.Position)) @@ -988,15 +1088,16 @@ func (x *ofdXML) shading(name string, attrs ofdAttrs, segments []ShdSegment) { // element 写出具有标准XML标签的子树,显式声明OFD命名空间 // 入参: name 元素名称, value 元素内容 func (x *ofdXML) element(name string, value any) { - if x.err == nil { + if x.checkContext() { x.err = x.encoder.EncodeElement(value, xml.StartElement{Name: xml.Name{Space: ofdNamespace, Local: name}}) + x.referenceElement(name, value) } } // border 写出图片边框及其绘制颜色 // 入参: border 边框,nil不输出节点 func (x *ofdXML) border(border *ImageBorder) { - if border == nil { + if border == nil || !x.checkContext() { return } var attrs ofdAttrs @@ -1015,6 +1116,9 @@ func (x *ofdXML) border(border *ImageBorder) { // pattern 写出图案单元,保留对象绘制顺序 // 入参: pattern 图案 func (x *ofdXML) pattern(pattern *Pattern) { + if !x.checkContext() { + return + } var attrs ofdAttrs attrs.number("Width", pattern.Width) attrs.number("Height", pattern.Height) @@ -1028,6 +1132,9 @@ func (x *ofdXML) pattern(pattern *Pattern) { content.add("Thumbnail", pattern.CellContent.Thumbnail) x.start("CellContent", content) for object := range pattern.CellContent.objects() { + if x.err != nil { + return + } x.object(object, false) } x.end("CellContent") @@ -1037,19 +1144,28 @@ func (x *ofdXML) pattern(pattern *Pattern) { // actions 写出对象动作及精确点击区域,不检查链接可达性 // 入参: actions 动作列表 func (x *ofdXML) actions(actions []Action) { - if len(actions) == 0 { + if len(actions) == 0 || !x.checkContext() { return } x.start("Actions", nil) for _, action := range actions { + if x.err != nil { + return + } var attrs ofdAttrs attrs.add("Event", action.Event) x.start("Action", attrs) if action.Region != nil { x.start("Region", nil) for _, area := range action.Region.Area { + if x.err != nil { + return + } x.start("Area", ofdAttrs{{Name: xml.Name{Local: "Start"}, Value: area.Start}}) for _, command := range area.Command { + if x.err != nil { + return + } var attrs ofdAttrs for _, pair := range [][2]string{{"Point1", command.Point1}, {"Point2", command.Point2}, {"Point3", command.Point3}, {"EllipseSize", command.EllipseSize}, {"RotationAngle", command.RotationAngle}, {"LargeArc", command.LargeArc}, {"SweepDirection", command.SweepDirection}, {"EndPoint", command.EndPoint}} { attrs.add(pair[0], pair[1])