From dfd4f733f936b4eaa31607be58202a3e83f07e54 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Tue, 27 Oct 2026 11:54:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E7=BC=96=E8=BE=91=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E4=B8=8E?= =?UTF-8?q?=E5=B5=8C=E5=A5=97=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 5 +- ofdgo_editor.go | 4 +- ofdgo_editor_annot.go | 29 ++++- ofdgo_editor_batch.go | 10 +- ofdgo_editor_composite_copy.go | 22 ++-- ofdgo_editor_composite_ops.go | 16 ++- ofdgo_editor_rich_text.go | 207 ++++++++++++++++++++------------- ofdgo_page.go | 2 +- 8 files changed, 184 insertions(+), 111 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 4e4d5fb..668278a 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -10505,7 +10505,9 @@ function copyEditorSelection(event) { return callTabWASM(tab, "ofdgoCaptureObjects", index, ids, clipboard.token).then(async result => { clipboard.data = result.bytes; clipboard.cut = false; - if (cut && isForegroundDocument(tab) && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex && state.objectClipboardToken === clipboard.token && workspace.objectClipboard === clipboard) { + if (cut && isForegroundDocument(tab) && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex + && state.objectClipboardToken === clipboard.token && workspace.objectClipboard === clipboard + && state.editing && !formDialogOpen() && !canvasEditor.input && !canvasEditor.crop && !canvasEditor.drag && !canvasEditor.nudge && !canvasEditor.nudgeCommit) { clipboard.cut = Boolean(await changeDocument("ofdgoDeleteObjects", { index, id: ids, items })); } return true; @@ -10548,6 +10550,7 @@ async function pasteEditorContent(event) { } const openSeq = state.openSeq; if (!await clipboard.ready || !isForegroundDocument(tab) || openSeq !== state.openSeq || index !== state.pageIndex || workspace.objectClipboard !== clipboard) return; + if (!state.editing || formDialogOpen() || document.body.hasAttribute("data-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag || canvasEditor.nudge || canvasEditor.nudgeCommit) return; if (!pageCan("insert")) return; const visible = visiblePageBounds(index), bounds = clipboard.bounds; const target = `${tab.id}:${state.wasmSeq}:${state.doc.documentIndex || 0}:${state.doc.documentVersion || ""}`; diff --git a/ofdgo_editor.go b/ofdgo_editor.go index a6f4133..6e0d494 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -631,7 +631,9 @@ func (e *Editor) Object(page int, id string) (GraphicObject, error) { } object, err := cloneEditorObject(layer.Objects[index]) object.origin = e.objectOrigin(id) - e.attachRichTextSnapshot(page, &object) + if err := e.attachRichTextSnapshot(page, &object); err != nil { + return GraphicObject{}, err + } return object, err } diff --git a/ofdgo_editor_annot.go b/ofdgo_editor_annot.go index 5cbe1c9..838db54 100644 --- a/ofdgo_editor_annot.go +++ b/ofdgo_editor_annot.go @@ -32,6 +32,7 @@ type AnnotationSelection struct { data [][]byte resources []string states map[string]map[string]editorCompositeState + richText map[int]*richTextSnapshot } // Annotations 获取指定页面的注解信息,包含不可见注解,返回值与编辑器相互独立 @@ -317,7 +318,7 @@ func (e *Editor) CaptureAnnotations(page int, ids []string) (*AnnotationSelectio if err != nil { return nil, err } - selection := &AnnotationSelection{editor: e, document: e.documentIdentity(), states: make(map[string]map[string]editorCompositeState)} + selection := &AnnotationSelection{editor: e, document: e.documentIdentity(), states: make(map[string]map[string]editorCompositeState), richText: make(map[int]*richTextSnapshot)} seen := make(map[string]bool) for _, id := range ids { key := editorResourceID(id) @@ -336,6 +337,17 @@ func (e *Editor) CaptureAnnotations(page int, ids []string) (*AnnotationSelectio if !editorXMLCopyable(root) { return nil, fmt.Errorf("annotation contains unsupported identifiers") } + annotation, err := e.Annotation(page, id) + if err != nil { + return nil, err + } + snapshot, err := e.captureRichTextSnapshot(page, GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: CompositeGraphicUnit{Objects: annotation.Appearance.Objects}}) + if err != nil { + return nil, err + } + if snapshot != nil { + selection.richText[len(selection.data)] = snapshot + } selection.data = append(selection.data, data) selection.states[key] = maps.Clone(e.source.annotationStates[e.pages[page].ID+"/"+key]) } @@ -452,7 +464,20 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx, break } } - return edit.commitAnnotationParts(edit.source, map[string][]byte{name: editorPatchXML(data, []editorXMLPatch{{position, position, added}})}) + if err := edit.commitAnnotationParts(edit.source, map[string][]byte{name: editorPatchXML(data, []editorXMLPatch{{position, position, added}})}); err != nil { + return err + } + } + } + for i, id := range result { + if snapshot := selection.richText[i]; snapshot != nil { + annotation, err := edit.Annotation(page, id) + if err != nil { + return err + } + if err := edit.putRichTextSnapshot(page, GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: CompositeGraphicUnit{Objects: annotation.Appearance.Objects}}, snapshot); err != nil { + return err + } } } return nil diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index f087cdc..919d10a 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -96,7 +96,9 @@ func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) { return nil, err } objects[i].origin = e.objectOrigin(editorObjectID(object)) - e.attachRichTextSnapshot(page, &objects[i]) + if err := e.attachRichTextSnapshot(page, &objects[i]); err != nil { + return nil, err + } } return objects, nil } @@ -135,7 +137,11 @@ func (e *Editor) CopyObjectsContext(ctx context.Context, page int, objects []Gra if err := ctx.Err(); err != nil { return err } - if err := e.putRichTextMetadata(page, ids[i], meta, -1); err != nil { + object, err := e.Object(page, ids[i]) + if err != nil { + return err + } + if err := e.putRichTextSnapshot(page, object, meta); err != nil { return err } } diff --git a/ofdgo_editor_composite_copy.go b/ofdgo_editor_composite_copy.go index 0f181b1..a4470d9 100644 --- a/ofdgo_editor_composite_copy.go +++ b/ofdgo_editor_composite_copy.go @@ -17,7 +17,6 @@ package ofdgo import ( "bytes" "encoding/xml" - "errors" "fmt" "maps" "slices" @@ -37,7 +36,7 @@ type CompositeSelection struct { containers []int nodes []*editorCompositeNode source *editorSourcePage - richText map[int]richTextMetadata + richText map[int]*richTextSnapshot } // PasteCompositeSelection 将内部对象快照粘贴到页面顶层,保持嵌套结构、源继承外观和裁剪 @@ -75,7 +74,7 @@ func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []in slices.Sort(indexes) var owner *editorCompositeNode var container *editorXML - selection := &CompositeSelection{richText: make(map[int]richTextMetadata), editor: e, document: e.documentIdentity(), page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: slices.Clone(path.Children)}} + selection := &CompositeSelection{richText: make(map[int]*richTextSnapshot), editor: e, document: e.documentIdentity(), page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: slices.Clone(path.Children)}} if path.Annotation == "" { if origin := e.objectOrigin(path.ID); origin != nil { selection.source = origin.page @@ -99,11 +98,13 @@ func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []in copy := *node copy.owner, copy.span = nil, nil selection.nodes = append(selection.nodes, ©) - if entry, err := e.richTextEntry(page, editorObjectID(node.object)); err == nil { - selection.richText[j] = cloneEditorData(entry.metadata) - } else if !errors.Is(err, ErrNotRichText) { + snapshot, err := e.captureRichTextSnapshot(page, node.object) + if err != nil { return nil, err } + if snapshot != nil { + selection.richText[j] = snapshot + } } scope := root.loadedScope(path.Children) for scope != owner { @@ -344,14 +345,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu copy.object.CompositeGraphicUnit.states = object.CompositeGraphicUnit.states } object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), page: selection.source, data: copy.data, node: copy.node, object: copy.object} - if meta, ok := selection.richText[i]; ok { - unit, err := e.compositeDefinition(object.CompositeGraphicUnit.ResourceID) - if err != nil { - return err - } - meta.Digest = richTextDigest(unit.object.CompositeGraphicUnit.Objects) - object.richText = &meta - } + object.richText = selection.richText[i] objects = append(objects, object) remaps = append(remaps, copy.remaps...) } diff --git a/ofdgo_editor_composite_ops.go b/ofdgo_editor_composite_ops.go index 196b12a..404f148 100644 --- a/ofdgo_editor_composite_ops.go +++ b/ofdgo_editor_composite_ops.go @@ -17,7 +17,6 @@ package ofdgo import ( "bytes" "context" - "errors" "fmt" "maps" "slices" @@ -46,7 +45,7 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int, return nil, fmt.Errorf("copy requires finite offsets") } var result []int - metadata := make(map[int]richTextMetadata) + metadata := make(map[int]*richTextSnapshot) err := e.Transaction(func(e *Editor) error { err := e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error { container := nodes[0].span.parent @@ -57,13 +56,12 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int, } slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index }) for i, node := range nodes { - if node.object.Type == "CompositeObject" { - entry, err := e.richTextEntry(page, editorObjectID(node.object)) - if err == nil { - metadata[i] = entry.metadata - } else if !errors.Is(err, ErrNotRichText) { - return err - } + snapshot, err := e.captureRichTextSnapshot(page, node.object) + if err != nil { + return err + } + if snapshot != nil { + metadata[i] = snapshot } } data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy) diff --git a/ofdgo_editor_rich_text.go b/ofdgo_editor_rich_text.go index 08a751c..b004284 100644 --- a/ofdgo_editor_rich_text.go +++ b/ofdgo_editor_rich_text.go @@ -46,6 +46,12 @@ type richTextMetadata struct { Digest string `json:"digest"` } +// richTextSnapshot 保存对象及嵌套成员的不可变编辑语义,成员按绘制顺序定位 +type richTextSnapshot struct { + metadata *richTextMetadata + children map[int]*richTextSnapshot +} + // richTextEntry 保存标引入口及延迟报告的损坏诊断 type richTextEntry struct { index int @@ -424,12 +430,12 @@ func (e *Editor) putCompositeRichTextMetadata(page int, object GraphicObject, me // putCompositeRichTextCopies 一次读取复制后的选区,按对象顺序恢复富文本标引 // 入参: page 页面索引, path 父路径, indexes 新成员序号, metadata 选区位置对应的编辑语义 // 返回: error 读取或提交错误 -func (e *Editor) putCompositeRichTextCopies(page int, path ObjectPath, indexes []int, metadata map[int]richTextMetadata) error { +func (e *Editor) putCompositeRichTextCopies(page int, path ObjectPath, indexes []int, metadata map[int]*richTextSnapshot) error { if len(metadata) == 0 { return nil } selected := make([]int, 0, len(metadata)) - values := make([]richTextMetadata, 0, len(metadata)) + values := make([]*richTextSnapshot, 0, len(metadata)) for i, index := range indexes { if meta, ok := metadata[i]; ok { selected = append(selected, index) @@ -441,7 +447,7 @@ func (e *Editor) putCompositeRichTextCopies(page int, path ObjectPath, indexes [ return err } for i, member := range members { - if err := e.putCompositeRichTextMetadata(page, member.Object, values[i], -1); err != nil { + if err := e.putRichTextSnapshot(page, member.Object, values[i]); err != nil { return err } } @@ -779,52 +785,18 @@ func (e *Editor) copyRichTextMetadata(ctx context.Context, source *Editor, sourc if metadata.err != nil || len(metadata.entries) == 0 { return metadata.err } - seen := make(map[string]bool) - var copyObject func(GraphicObject, GraphicObject) error - copyObject = func(before, after GraphicObject) error { + copyObject := func(before, after GraphicObject) error { if err := ctx.Err(); err != nil { return err } - if before.Type != "CompositeObject" && before.Type != "CompositeGraphicUnit" { - return nil - } - id := editorObjectID(after) - if id != "" { - if seen[id] { - return nil - } - seen[id] = true - } - entry, err := source.richTextEntry(sourcePage, editorObjectID(before)) - if err == nil { - if _, err := source.richTextObject(sourcePage, before); err != nil { - return err - } - return e.putCompositeRichTextMetadata(page, after, entry.metadata, -1) - } - if !errors.Is(err, ErrNotRichText) { + snapshot, err := source.captureRichTextSnapshot(sourcePage, before) + if err != nil || snapshot == nil { return err } - units := []CompositeGraphicUnit{before.CompositeGraphicUnit, after.CompositeGraphicUnit} - for i, owner := range []*Editor{source, e} { - if units[i].ResourceID != "" { - definition, err := owner.compositeDefinition(units[i].ResourceID) - if err != nil { - return err - } - units[i] = definition.object.CompositeGraphicUnit - } + if err := e.putRichTextSnapshot(page, after, snapshot); err != nil { + return err } - original, target := slices.Collect(units[0].objects()), slices.Collect(units[1].objects()) - if len(original) != len(target) { - return fmt.Errorf("imported composite structure differs") - } - for i := range original { - if err := copyObject(original[i], target[i]); err != nil { - return err - } - } - return nil + return ctx.Err() } for i, id := range ids { before, err := source.Object(sourcePage, id) @@ -887,25 +859,18 @@ func (e *Editor) richTextColors(object *GraphicObject) error { // copiedRichTextMetadata 检查当前文档对象快照的标引,不将外部同号对象误认成本地对象 // 入参: objects 待复制快照 -// 返回: map[int]richTextMetadata 需要复制的语义, error 损坏诊断 -func (e *Editor) copiedRichTextMetadata(objects []GraphicObject) (map[int]richTextMetadata, error) { - result := make(map[int]richTextMetadata) +// 返回: map[int]*richTextSnapshot 需要复制的语义, error 损坏诊断 +func (e *Editor) copiedRichTextMetadata(objects []GraphicObject) (map[int]*richTextSnapshot, error) { + result := make(map[int]*richTextSnapshot) for i, object := range objects { - if object.Type != "CompositeObject" { + if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" { continue } if object.origin != nil && e.snapshotOrigin(object) == nil { continue } if object.richText != nil { - unit, err := e.compositeDefinition(object.CompositeGraphicUnit.ResourceID) - if err != nil { - return nil, err - } - if richTextDigest(unit.object.CompositeGraphicUnit.Objects) != object.richText.Digest { - return nil, fmt.Errorf("rich text snapshot resource has changed") - } - result[i] = cloneEditorData(*object.richText) + result[i] = object.richText continue } index := e.richTextMetadataIndex() @@ -915,45 +880,125 @@ func (e *Editor) copiedRichTextMetadata(objects []GraphicObject) (map[int]richTe if len(index.entries) == 0 { continue } - id := editorObjectID(object) for page := range e.pages { - entry, err := e.richTextEntry(page, id) - if errors.Is(err, ErrNotRichText) { - continue - } + snapshot, err := e.captureRichTextSnapshot(page, object) if err != nil { return nil, err } - current, err := e.Object(page, id) - if err != nil { - continue + if snapshot != nil { + result[i] = snapshot + break } - if current.CompositeGraphicUnit.ResourceID != object.CompositeGraphicUnit.ResourceID { - continue - } - if _, err := e.RichText(page, id); err != nil { - return nil, err - } - result[i] = entry.metadata - break } } return result, nil } -// attachRichTextSnapshot 为普通对象快照附加不可变编辑语义,复制后不依赖原对象继续存在 +// attachRichTextSnapshot 为对象快照附加嵌套编辑语义,来源删除后仍可跨页粘贴 // 入参: page 页面索引, object 对象快照 -func (e *Editor) attachRichTextSnapshot(page int, object *GraphicObject) { - object.richText = nil - if object.Type != "CompositeObject" { - return +// 返回: error 标引或资源错误 +func (e *Editor) attachRichTextSnapshot(page int, object *GraphicObject) error { + snapshot, err := e.captureRichTextSnapshot(page, *object) + object.richText = snapshot + return err +} + +// captureRichTextSnapshot 捕获组合树内的富文本标引,不复制共享二进制资源 +// 入参: page 页面索引, object 根对象 +// 返回: *richTextSnapshot 不可变快照,无标引时为空, error 标引或资源错误 +func (e *Editor) captureRichTextSnapshot(page int, object GraphicObject) (*richTextSnapshot, error) { + if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" { + return nil, nil } - entry, err := e.richTextEntry(page, editorObjectID(*object)) - if err != nil { - return + index := e.richTextMetadataIndex() + if index.err != nil || len(index.entries) == 0 { + return nil, index.err } - meta := cloneEditorData(entry.metadata) - object.richText = &meta + active := make(map[string]bool) + var capture func(GraphicObject) (*richTextSnapshot, error) + capture = func(object GraphicObject) (*richTextSnapshot, error) { + if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" { + return nil, nil + } + entry, err := e.richTextEntry(page, editorObjectID(object)) + if err == nil { + if _, err := e.richTextObject(page, object); err != nil { + return nil, err + } + meta := cloneEditorData(entry.metadata) + return &richTextSnapshot{metadata: &meta}, nil + } + if !errors.Is(err, ErrNotRichText) { + return nil, err + } + unit := object.CompositeGraphicUnit + if unit.ResourceID != "" { + if active[unit.ResourceID] { + return nil, fmt.Errorf("cyclic composite resource") + } + active[unit.ResourceID] = true + defer delete(active, unit.ResourceID) + definition, err := e.compositeDefinition(unit.ResourceID) + if err != nil { + return nil, err + } + unit = definition.object.CompositeGraphicUnit + } + var snapshot *richTextSnapshot + i := 0 + for child := range unit.objects() { + value, err := capture(child) + if err != nil { + return nil, err + } + if value != nil { + if snapshot == nil { + snapshot = &richTextSnapshot{children: make(map[int]*richTextSnapshot)} + } + snapshot.children[i] = value + } + i++ + } + return snapshot, nil + } + return capture(object) +} + +// putRichTextSnapshot 恢复复制后的嵌套标引,按目标标准内容更新摘要和字体映射 +// 入参: page 目标页, object 复制后的对象, snapshot 来源不可变语义 +// 返回: error 结构或标引错误 +func (e *Editor) putRichTextSnapshot(page int, object GraphicObject, snapshot *richTextSnapshot) error { + if snapshot.metadata != nil { + index := -1 + if entry, err := e.richTextEntry(page, editorObjectID(object)); err == nil { + index = entry.index + } else if !errors.Is(err, ErrNotRichText) { + return err + } + return e.putCompositeRichTextMetadata(page, object, *snapshot.metadata, index) + } + unit := object.CompositeGraphicUnit + if unit.ResourceID != "" { + definition, err := e.compositeDefinition(unit.ResourceID) + if err != nil { + return err + } + unit = definition.object.CompositeGraphicUnit + } + i, count := 0, 0 + for child := range unit.objects() { + if value := snapshot.children[i]; value != nil { + if err := e.putRichTextSnapshot(page, child, value); err != nil { + return err + } + count++ + } + i++ + } + if count != len(snapshot.children) { + return fmt.Errorf("copied composite structure differs") + } + return nil } // richTextTagsFontSafe 仅允许完整识别且不含资源编号的本库标引通过字体重绑定检查 diff --git a/ofdgo_page.go b/ofdgo_page.go index 0a30e0a..cf04d1f 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -63,7 +63,7 @@ type GraphicObject struct { CompositeGraphicUnit CompositeGraphicUnit origin *editorObjectOrigin state editorCompositeState - richText *richTextMetadata + richText *richTextSnapshot } // Clips 相对于对象外接矩形的裁剪集合