From e4af1e73189880c4d727c7bcc25a65391e18951f Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 30 Sep 2026 14:26:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E4=BF=AE=E5=A4=8D=E9=97=AE=E9=A2=98):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=BE=91=E7=BC=BA=E9=99=B7=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=B5=84=E6=BA=90=E5=9B=9E=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 1 + go.mod | 2 +- go.sum | 4 +- internal/webui/wasm.go | 39 ++++------------- ofdgo_editor_annot_ops.go | 11 ++++- ofdgo_editor_attachment.go | 22 +++++++--- ofdgo_editor_batch.go | 17 ++++++-- ofdgo_editor_selection_copy.go | 78 ++++++++++++++++++++++++++++++++++ 8 files changed, 130 insertions(+), 44 deletions(-) create mode 100644 ofdgo_editor_selection_copy.go diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index a39c804..fb903d4 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -5687,6 +5687,7 @@ async function processPageRenderQueue() { } } finally { state.pageRenderRunning = false; + if (!state.pageInFlight.size) releasePageResources(); } } diff --git a/go.mod b/go.mod index a6a2775..18e96d1 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20260930050241-ecc0b0d74180 + github.com/xiaoqidun/pdfgo v0.0.0-20260930062146-7c39a20d9430 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 1f73e12..6ffe5cb 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-20260930050241-ecc0b0d74180 h1:2/QHfh0NIg74xzT3KHjqN7u1S9U2F+4N0HZqPrJFxvw= -github.com/xiaoqidun/pdfgo v0.0.0-20260930050241-ecc0b0d74180/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260930062146-7c39a20d9430 h1:UBP+ZmqxoxQ7p6dFZI/6yGSLmgqfTQUvO7h+DUfWsWY= +github.com/xiaoqidun/pdfgo v0.0.0-20260930062146-7c39a20d9430/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 4cd23fa..593eedc 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -82,10 +82,9 @@ type apiResult struct { // editorClipboard 当前编辑文档中的对象快照与剪贴板标识 type editorClipboard struct { - token string - objects []ofdgo.GraphicObject - composite *ofdgo.CompositeSelection - annotations *ofdgo.AnnotationSelection + token string + selection *ofdgo.ObjectSelection + composite *ofdgo.CompositeSelection } // editorInfo 编辑文档信息与操作状态 @@ -1669,10 +1668,7 @@ func captureEditorSelection(page int, ids []string) (*editorClipboard, error) { objects, annotations := splitEditorSelection(ids) result := &editorClipboard{} var err error - result.objects, err = currentEditor.Objects(page, objects) - if err == nil && len(annotations) != 0 { - result.annotations, err = currentEditor.CaptureAnnotations(page, annotations) - } + result.selection, err = currentEditor.CaptureSelection(page, objects, annotations) return result, err } @@ -1680,24 +1676,10 @@ func captureEditorSelection(page int, ids []string) (*editorClipboard, error) { // 入参: page 目标页, clipboard 快照, dx、dy 位移 // 返回: []string 新画布标识, error 错误信息 func pasteEditorSelection(page int, clipboard *editorClipboard, dx, dy float64) ([]string, error) { - var ids []string - err := currentEditor.Transaction(func(edit *ofdgo.Editor) error { - var err error - ids, err = edit.CopyObjects(page, clipboard.objects, dx, dy) - if err != nil { - return err - } - if clipboard.annotations != nil { - annotations, err := edit.PasteAnnotations(page, clipboard.annotations, dx, dy) - if err != nil { - return err - } - for _, id := range annotations { - ids = append(ids, "annotation:"+id) - } - } - return nil - }) + ids, annotations, err := currentEditor.PasteSelection(page, clipboard.selection, dx, dy) + for _, id := range annotations { + ids = append(ids, "annotation:"+id) + } return ids, err } @@ -2677,9 +2659,6 @@ func pasteObjects(args []js.Value) (any, error) { } page, dx, dy := args[0].Int(), args[2].Float(), args[3].Float() if key != "" { - if copiedObjects.annotations != nil { - return nil, fmt.Errorf("annotations cannot be nested in a page object") - } path, err := compositePath(key) if err != nil { return nil, err @@ -2688,7 +2667,7 @@ func pasteObjects(args []js.Value) (any, error) { if copiedObjects.composite != nil { indexes, err = currentEditor.PasteCompositeObjects(page, path, copiedObjects.composite, dx, dy) } else { - indexes, err = currentEditor.CopyObjectsToComposite(page, path, copiedObjects.objects, dx, dy) + indexes, err = currentEditor.PasteSelectionToComposite(page, path, copiedObjects.selection, dx, dy) } if err != nil { return nil, err diff --git a/ofdgo_editor_annot_ops.go b/ofdgo_editor_annot_ops.go index 090bbc5..994fd2d 100644 --- a/ofdgo_editor_annot_ops.go +++ b/ofdgo_editor_annot_ops.go @@ -19,7 +19,6 @@ import ( "encoding/xml" "fmt" "maps" - "slices" ) // AnnotationSelection 保存当前编辑器内可重复粘贴的注解原文及资源上下文 @@ -151,10 +150,18 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx, return err } common := root.child("CommonData") + registered := make(map[string]bool) + for _, name := range edit.source.document.CommonData.PublicRes { + registered[reader.ResPath(name)] = true + } + for _, name := range edit.source.document.CommonData.DocumentRes { + registered[reader.ResPath(name)] = true + } var added []byte for _, name := range selection.resources { - if !slices.ContainsFunc(append(slices.Clone(edit.source.document.CommonData.PublicRes), edit.source.document.CommonData.DocumentRes...), func(value string) bool { return reader.ResPath(value) == name }) { + if !registered[name] { added = append(added, editorXMLText("DocumentRes", "/"+name)...) + registered[name] = true } } if len(added) != 0 { diff --git a/ofdgo_editor_attachment.go b/ofdgo_editor_attachment.go index 5223b3d..589ce6a 100644 --- a/ofdgo_editor_attachment.go +++ b/ofdgo_editor_attachment.go @@ -20,6 +20,7 @@ import ( "fmt" "maps" "path" + "strconv" "strings" "unicode/utf8" ) @@ -40,18 +41,18 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) { if err := validateAttachmentName(name); err != nil { return "", err } - if err := e.prepareSourceIDs(); err != nil { + maximum, err := e.sourceMaxID(nil) + if err != nil { return "", err } base := e.source - var err error if base == nil { base, err = e.importBase() if err != nil { return "", err } } - id := e.nextID() + id := strconv.Itoa(maximum + 1) file := e.packageName("Attachments/Attachment_" + id) attrs := ofdAttrs{} attrs.add("ID", id) @@ -112,6 +113,7 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) { if err := e.commitAnnotationParts(base, parts); err != nil { return "", err } + e.maxID, e.source.idsReady = maximum+1, true return id, nil } @@ -134,11 +136,14 @@ func (e *Editor) RenameAttachment(id, name string) error { // 入参: id 附件标识, data 文件数据,调用后可复用 // 返回: error 附件结构或提交错误 func (e *Editor) ReplaceAttachment(id string, data []byte) error { - return e.editAttachment(id, func(fragment []byte, node *editorXML, parts map[string][]byte) ([]byte, error) { - if err := e.prepareSourceIDs(); err != nil { + var maximum int + err := e.editAttachment(id, func(fragment []byte, node *editorXML, parts map[string][]byte) ([]byte, error) { + var err error + maximum, err = e.sourceMaxID(nil) + if err != nil { return nil, err } - file := e.packageName("Attachments/Attachment_" + e.nextID()) + file := e.packageName("Attachments/Attachment_" + strconv.Itoa(maximum+1)) var location bytes.Buffer if err := xml.EscapeText(&location, []byte("/"+file)); err != nil { return nil, err @@ -161,6 +166,11 @@ func (e *Editor) ReplaceAttachment(id string, data []byte) error { parts[file] = bytes.Clone(data) return fragment, nil }) + if err != nil { + return err + } + e.maxID, e.source.idsReady = maximum+1, true + return nil } // DeleteAttachment 删除附件声明,保存时清理可确认无引用的内容,支持撤销和重做 diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index 83d4e99..c63fcba 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -212,10 +212,15 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error { } layer := &e.pages[page].Content.Layer[indexes[0].layer] _, siblings := e.objectOrderIndexes(page, layer, ids[0]) + positions := make(map[int]int, len(siblings)) + for i, index := range siblings { + positions[index] = i + } orderable := make(map[*editorXML]bool) for i, object := range selectedObjects { capability := e.objectCapabilities(object, orderable) - if !capability.Order || indexes[i].layer != indexes[0].layer || !slices.Contains(siblings, indexes[i].index) { + _, exists := positions[indexes[i].index] + if !capability.Order || indexes[i].layer != indexes[0].layer || !exists { return fmt.Errorf("ordering requires editable objects in the same container") } } @@ -226,7 +231,7 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error { } selected := make([]bool, len(objects)) for _, index := range indexes { - selected[slices.Index(siblings, index.index)] = true + selected[positions[index.index]] = true } orderEditorObjects(objects, selected, order) for i, index := range siblings { @@ -507,11 +512,17 @@ func editorDistribution(boxes []Box, indexes []editorObjectPosition, axis string // replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作 // 入参: page 页面索引, layers 新图层列表 func (e *Editor) replaceLayers(page int, layers []Layer) { - before := copyEditorPage(e.pages[page]).Content.Layer + before := e.pages[page].Content.Layer if reflect.DeepEqual(before, layers) { return } after := copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer + if e.historyLimit == 0 { + e.pages[page].Content.Layer = after + e.recordChange() + return + } + before = copyEditorPage(PageContent{Content: Content{Layer: before}}).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_selection_copy.go b/ofdgo_editor_selection_copy.go new file mode 100644 index 0000000..af9ab33 --- /dev/null +++ b/ofdgo_editor_selection_copy.go @@ -0,0 +1,78 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import "fmt" + +// ObjectSelection 保存正文与注解的独立快照,共享当前编辑器的不可变资源 +// 来源修改、删除或撤销不改变快照,不用于跨文档传输 +type ObjectSelection struct { + editor *Editor + objects []GraphicObject + annotations *AnnotationSelection +} + +// CaptureSelection 捕获正文与注解,不修改文档或历史 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复 +// 返回: *ObjectSelection 独立快照, error 标识或资源错误 +func (e *Editor) CaptureSelection(page int, objects, annotations []string) (*ObjectSelection, error) { + items, err := e.Objects(page, objects) + if err != nil { + return nil, err + } + selection := &ObjectSelection{editor: e, objects: items} + if len(annotations) != 0 { + selection.annotations, err = e.CaptureAnnotations(page, annotations) + if err != nil { + return nil, err + } + } + return selection, nil +} + +// PasteSelection 原子粘贴正文与注解并平移,失败回滚,一次撤销恢复全部 +// 入参: page 目标页面索引, selection 当前编辑器快照, dx、dy 页面毫米位移 +// 返回: []string 新正文标识, []string 新注解标识, error 标识、资源或编辑错误 +func (e *Editor) PasteSelection(page int, selection *ObjectSelection, dx, dy float64) ([]string, []string, error) { + if selection == nil || selection.editor != e || !finite(dx) || !finite(dy) { + return nil, nil, fmt.Errorf("invalid object selection or offset") + } + var objects, annotations []string + err := e.Transaction(func(edit *Editor) error { + var err error + objects, err = edit.CopyObjects(page, selection.objects, dx, dy) + if err == nil && selection.annotations != nil { + annotations, err = edit.PasteAnnotations(page, selection.annotations, dx, dy) + } + return err + }) + if err != nil { + return nil, nil, err + } + return objects, annotations, nil +} + +// PasteSelectionToComposite 将正文快照粘贴至内部容器,保留源外观,拒绝嵌套注解 +// 入参: page 目标页面索引, path 父复合路径, selection 当前编辑器快照, dx、dy 页面毫米位移 +// 返回: []int 新成员序号, error 选区、资源或编辑错误 +func (e *Editor) PasteSelectionToComposite(page int, path ObjectPath, selection *ObjectSelection, dx, dy float64) ([]int, error) { + if selection == nil || selection.editor != e { + return nil, fmt.Errorf("object selection belongs to another editor") + } + if selection.annotations != nil { + return nil, fmt.Errorf("annotations cannot be nested in a page object") + } + return e.CopyObjectsToComposite(page, path, selection.objects, dx, dy) +}