From e7686b5430de7179269d86ad5dd6c535363463d7 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Fri, 18 Sep 2026 20:28:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BC=96=E8=BE=91=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=A4=8D=E6=9D=82=E6=96=87=E6=A1=A3=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E6=80=A7=E8=83=BD=EF=BC=8C=E4=BF=9D=E6=8C=81=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo_edit.js | 34 +++++++------ internal/webui/wasm.go | 14 +++--- ofdgo_editor_batch.go | 99 +++++++++++++++++++++++++++++--------- ofdgo_editor_open.go | 27 ++++++++--- ofdgo_font_info.go | 7 +++ 5 files changed, 129 insertions(+), 52 deletions(-) diff --git a/assets/webui/ofdgo_edit.js b/assets/webui/ofdgo_edit.js index 1a56d39..0f12cf3 100644 --- a/assets/webui/ofdgo_edit.js +++ b/assets/webui/ofdgo_edit.js @@ -307,8 +307,8 @@ export class CanvasEditor { const crop = this.crop?.item.index === index ? this.crop : null; if (crop) this.closeCrop(true); const pending = this.pendingSelection?.index === index; - const selected = pending ? this.pendingSelection.ids || page.objects.slice(-1).map(item => item.id) - : this.items().filter(item => item.index === index).map(item => item.id); + const selected = new Set(pending ? this.pendingSelection.ids || page.objects.slice(-1).map(item => item.id) + : this.items().filter(item => item.index === index).map(item => item.id)); const selection = []; this.pages.set(surface, { index, width: page.width, height: page.height }); const layer = document.createElement("div"); @@ -359,24 +359,14 @@ export class CanvasEditor { } this.nodes.set(node, item); node.addEventListener("focus", () => this.select(item)); - const handles = !canEditObject(object, "transform") ? [] : lineShape(object.shape) ? ["start", "end"] - : object.shape || object.oriented || canEditObject(object, "stretch") ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"] - : object.type === "TextObject" && canEditObject(object, "reflow") && canEditObject(object, "layoutKnown") - ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"] : ["nw", "ne", "sw", "se"]; - for (const corner of handles) { - const handle = document.createElement("span"); - handle.className = `edit-handle edit-${corner}`; - handle.dataset.corner = corner; - node.append(handle); - } this.place(item); layer.append(node); - if (selected.includes(object.id)) { + if (selected.has(object.id)) { selection.push(item); } } surface.append(layer); - if (selected.length) this.setSelection(selection); + if (selected.size) this.setSelection(selection); if (pending) this.pendingSelection = null; if (crop && selection.length === 1 && selection[0].id === crop.item.id && ["x", "y", "width", "height"].every(key => selection[0][key] === crop.item[key])) { @@ -487,6 +477,9 @@ export class CanvasEditor { for (const item of this.items()) { item.node.classList.remove("selected", "multi-selected"); item.node.setAttribute("aria-pressed", "false"); + for (const handle of [...item.node.children]) { + if (handle.dataset.corner) handle.remove(); + } } this.selected = items.length < 2 ? items[0] || null : { ...selectionBounds(items), items, id: items.map(item => item.id), index: items[0].index, page: items[0].page, surface: items[0].surface }; @@ -503,6 +496,19 @@ export class CanvasEditor { this.nodes.set(node, this.selected); this.place(this.selected); this.selected.surface.append(node); + } else if (items.length === 1) { + const item = items[0]; + const handles = !canEditObject(item, "transform") ? [] : lineShape(item.shape) ? ["start", "end"] + : item.shape || item.oriented || canEditObject(item, "stretch") ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"] + : item.type === "TextObject" && canEditObject(item, "reflow") && canEditObject(item, "layoutKnown") + ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"] : ["nw", "ne", "sw", "se"]; + for (const corner of handles) { + const handle = document.createElement("span"); + handle.className = `edit-handle edit-${corner}`; + handle.dataset.corner = corner; + item.node.append(handle); + } + this.place(item); } for (const item of items) { item.node.classList.add("selected"); diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 2e0617c..f3cfe94 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -732,6 +732,10 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) { if err != nil { return nil, err } + info, err := currentEditor.PageObjectInfo(index) + if err != nil { + return nil, err + } textBoxes := make(map[string]ofdgo.Box) for _, run := range text.Runs { for _, box := range run.Boxes { @@ -770,10 +774,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) { default: continue } - capability, err := currentEditor.ObjectCapabilities(index, id) - if err != nil { - return nil, err - } + capability := info[id].Capabilities var box ofdgo.Box var contours []ofdgo.ObjectContour switch object.Type { @@ -786,10 +787,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) { continue } if box.W > 0 && box.H > 0 { - position, err := currentEditor.ObjectPosition(index, id) - if err != nil { - return nil, err - } + position := info[id].Position item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "position": position.Index, "count": position.Count, "container": position.Container, "capabilities": editorCapabilities(capability, object.Type)} editorAppearance(item, object, capability.Paint, editorPathScale(object.PathObject)) diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index 677e949..72ab5c0 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -144,6 +144,53 @@ type ObjectPosition struct { Count int } +// ObjectInfo 对象的编辑能力及直接容器位置 +type ObjectInfo struct { + Capabilities ObjectCapabilities + Position ObjectPosition +} + +// PageObjectInfo 批量获取页面对象能力与位置,不重复扫描同一容器 +// 每次按当前页面计算,变更和撤销后无需失效旧缓存 +// 入参: page 页面索引 +// 返回: map[string]ObjectInfo 对象标识与信息, error 错误信息 +func (e *Editor) PageObjectInfo(page int) (map[string]ObjectInfo, error) { + content, err := e.page(page) + if err != nil { + return nil, err + } + result := make(map[string]ObjectInfo) + orderable := make(map[*editorXML]bool) + for i := range content.Content.Layer { + layer := &content.Content.Layer[i] + nodes := e.objectOrderNodes(page, layer) + groups := make(map[*editorXML][]string) + for _, object := range layer.Objects { + id := editorObjectID(object) + parent := e.objectOrderParent(nodes, id) + groups[parent] = append(groups[parent], id) + if id != "" { + result[id] = ObjectInfo{Capabilities: e.objectCapabilities(object, orderable)} + } + } + for parent, ids := range groups { + container := layer.ID + if parent != nil { + container = parent.attr("ID") + } + for index, id := range ids { + if id == "" { + continue + } + info := result[id] + info.Position = ObjectPosition{Layer: layer.ID, Container: container, Index: index, Count: len(ids)} + result[id] = info + } + } + } + return result, nil +} + // ObjectPosition 获取对象的直接容器及排序位置 // 入参: page 页面索引, id 对象标识 // 返回: ObjectPosition 对象位置, error 错误信息 @@ -170,36 +217,44 @@ func (e *Editor) ObjectPosition(page int, id string) (ObjectPosition, error) { // 入参: page 页面索引, layer 图层, id 对象标识 // 返回: *editorXML 原始容器, []int 对象索引 func (e *Editor) objectOrderIndexes(page int, layer *Layer, id string) (*editorXML, []int) { - var nodes map[string]*editorXML - var parent *editorXML - if e.originalPage(page) { - source := e.source.pages[e.pages[page].ID] - if slices.ContainsFunc(source.original.Content.Layer, func(original Layer) bool { return original.ID == layer.ID }) { - nodes = source.nodes - } - } - if nodes != nil { - if node := nodes[id]; node != nil { - parent = node.parent - } else if origin := e.objectOrigin(id); origin != nil { - parent = origin.node.parent - } - } + nodes := e.objectOrderNodes(page, layer) + parent := e.objectOrderParent(nodes, id) var indexes []int for i, object := range layer.Objects { - node := nodes[editorObjectID(object)] - if node == nil && nodes != nil { - if origin := e.objectOrigin(editorObjectID(object)); origin != nil { - node = origin.node - } - } - if node == nil && parent == nil || node != nil && node.parent == parent { + if e.objectOrderParent(nodes, editorObjectID(object)) == parent { indexes = append(indexes, i) } } return parent, indexes } +// objectOrderNodes 获取原图层中的对象节点 +// 入参: page 页面索引, layer 当前图层 +// 返回: map[string]*editorXML 原节点映射 +func (e *Editor) objectOrderNodes(page int, layer *Layer) map[string]*editorXML { + if e.originalPage(page) { + source := e.source.pages[e.pages[page].ID] + if slices.ContainsFunc(source.original.Content.Layer, func(original Layer) bool { return original.ID == layer.ID }) { + return source.nodes + } + } + return nil +} + +// objectOrderParent 获取对象的原始直接容器,新对象沿用所在图层 +// 入参: nodes 原节点映射, id 对象标识 +// 返回: *editorXML 原始容器 +func (e *Editor) objectOrderParent(nodes map[string]*editorXML, id string) *editorXML { + if nodes != nil { + if node := nodes[id]; node != nil { + return node.parent + } else if origin := e.objectOrigin(id); origin != nil { + return origin.node.parent + } + } + return nil +} + // OrderObjects 调整同一图层或页块内选区的绘制顺序,保留各组选中及未选中对象的相对顺序 // 入参: page 页面索引, ids 对象标识, order 为up、down、top或bottom // 返回: error 错误信息 diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 08789e0..77cd88b 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -367,7 +367,14 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er if err != nil { return ObjectCapabilities{}, err } - object := layer.Objects[index] + return e.objectCapabilities(layer.Objects[index], make(map[*editorXML]bool)), nil +} + +// objectCapabilities 检查对象能力,在同次遍历中复用原容器排序检查 +// 入参: object 当前对象, orderable 原容器排序能力 +// 返回: ObjectCapabilities 操作能力 +func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorXML]bool) ObjectCapabilities { + id := editorObjectID(object) all := ObjectCapabilities{Update: true, Paint: object.Type == "TextObject" || object.Type == "PathObject", ReplaceFont: object.Type == "TextObject", Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true} all.ResetCrop = object.Type == "ImageObject" && object.state.crop != nil origin := e.objectOrigin(id) @@ -377,22 +384,26 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er all.ReplaceImage = object.Type == "ImageObject" all.CropImage = all.ReplaceImage all.FitImage = all.ReplaceImage && axisAlignedMatrix(NewMatrix(object.ImageObject.CTM)) - return all, nil + return all } node := origin.node if !editorXMLTransformable(node) { - return ObjectCapabilities{Reason: "object uses unsupported editing features", ReasonCode: EditUnsupportedObject}, nil + return ObjectCapabilities{Reason: "object uses unsupported editing features", ReasonCode: EditUnsupportedObject} } if origin.reason != nil && editReason(origin.reason) == EditUnsupportedStyle { - return ObjectCapabilities{Reason: origin.reason.Error(), ReasonCode: EditUnsupportedStyle}, nil + return ObjectCapabilities{Reason: origin.reason.Error(), ReasonCode: EditUnsupportedStyle} } if !editorXMLContainersSupported(node) { - return ObjectCapabilities{Reason: "object container uses unsupported editing features", ReasonCode: EditUnsupportedContainer}, nil + return ObjectCapabilities{Reason: "object container uses unsupported editing features", ReasonCode: EditUnsupportedContainer} } if err := validateEditorGeometry(object); err != nil { - return ObjectCapabilities{Reason: err.Error(), ReasonCode: EditInvalidObject}, nil + return ObjectCapabilities{Reason: err.Error(), ReasonCode: EditInvalidObject} + } + var checked bool + if all.Order, checked = orderable[node.parent]; !checked { + all.Order = editorContainerOrderable(node.parent) + orderable[node.parent] = all.Order } - all.Order = editorContainerOrderable(node.parent) all.Ungroup = all.Order && e.compositeUngroupable(node) && (object.CompositeGraphicUnit.ResourceID != "" || len(object.CompositeGraphicUnit.Objects) != 0) all.Copy = editorXMLCopyable(node) && (origin.reason == nil || object.Type == "PathObject" && editReason(origin.reason) == EditUnsupportedColor) if origin.reason != nil { @@ -422,7 +433,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er } } all.Stretch = all.Transform && e.objectStretchable(object, make(map[string]bool)) - return all, nil + return all } // objectOrigin 获取原对象或其副本的保真来源 diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index 9751683..7d6e0ce 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -353,6 +353,13 @@ func (p *fontUsagePage) decodeObjects(d *xml.Decoder, start xml.StartElement, de switch node.Name.Local { case "PageBlock": return p.decodeObjects(d, node, defaults) + case "TextObject": + var obj TextObject + if err := d.DecodeElement(&obj, &node); err != nil { + return err + } + p.renderer.countObjectFonts(GraphicObject{Type: "TextObject", TextObject: obj}, p.usage, defaults, nil) + return nil case "PathObject": var obj struct { PathObject