// 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" "crypto/sha256" "encoding/xml" "fmt" "maps" "slices" "strconv" ) // GroupCompositeObjects 组合内部同一容器的连续成员,保留绘制顺序及共享资源 // 入参: page 页面索引, path 父路径, indexes 至少两个成员序号 // 返回: int 新组合序号, error 错误信息 func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int) (int, error) { if len(indexes) < 2 { return 0, fmt.Errorf("grouping requires at least two objects") } var result int err := e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error { for _, member := range members { if !member.Capabilities.Copy || !member.Capabilities.Order { return fmt.Errorf("composite member cannot be grouped") } } slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index }) siblings := nodes[0].siblings() first := slices.Index(siblings, nodes[0]) copies := make([]*editorCompositeNode, len(nodes)) for i, node := range nodes { if first+i >= len(siblings) || siblings[first+i] != node { return fmt.Errorf("grouping requires consecutive members in the same container") } copy, err := newEditorCompositeNode(node.data) if err != nil { return err } copy.setStates(node.states) copy.defaults = node.defaults if !node.boundaryInCTM { if err := copy.convertCoordinates(node.parent, true); err != nil { return err } } copies[i] = copy } group, err := e.vectorInstance(renderer, copies) if err != nil { return err } for i, node := range nodes { var replacement []byte if i == 0 { replacement = bytes.TrimPrefix(group.data, []byte(xml.Header)) } node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, replacement}) } result = nodes[0].index return nil }) return result, err } // vectorInstance 将同一坐标系的内容封装为标准矢量资源,独立分配内部标识 // 入参: renderer 资源度量器, nodes 局部内容节点 // 返回: *editorCompositeNode 资源实例, error 尺寸、内容或资源错误 func (e *Editor) vectorInstance(renderer *Renderer, nodes []*editorCompositeNode) (*editorCompositeNode, error) { if err := e.prepareSourceIDs(); err != nil { return nil, err } var extent Box for _, node := range nodes { boundary, _ := editorGeometry(node.object) box, err := creationBox(boundary) if err != nil { return nil, err } visible, err := renderer.MeasureObject(node.object, MeasureOptions{Defaults: node.defaults}) if err != nil { return nil, err } extent = unionTextBox(extent, unionTextBox(box, visible.Bounds)) } if !finite(extent.X) || !finite(extent.Y) || !finite(extent.W) || !finite(extent.H) || extent.W <= 0 || extent.H <= 0 { return nil, fmt.Errorf("invalid vector dimensions") } var content []byte states := make(map[string]editorCompositeState) for _, node := range nodes { object := transformEditorMatrix(cloneEditorData(node.object), TranslationMatrix(-extent.X, -extent.Y)) data, err := editorXMLObject(node.data, node.node, node.object, object) if err != nil { return nil, err } content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...) maps.Copy(states, node.states) } content, err := editorXMLContainer("Content", nil, content) if err != nil { return nil, err } hash := sha256.New() hash.Write([]byte(ofdNumber(extent.W) + " " + ofdNumber(extent.H))) hash.Write(content) key := [32]byte(hash.Sum(nil)) var resource string for _, candidate := range e.resources { if candidate.vectorKey == key && maps.Equal(candidate.vectorStates, states) { resource = candidate.composite break } } if resource == "" { data, err := editorXMLContainer("CompositeGraphicUnit", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: strconv.Itoa(e.maxID + 1)}, {Name: xml.Name{Local: "Width"}, Value: ofdNumber(extent.W)}, {Name: xml.Name{Local: "Height"}, Value: ofdNumber(extent.H)}}, bytes.TrimPrefix(content, []byte(xml.Header))) if err != nil { return nil, err } resource, err = e.addCompositeResource(data, states) if err != nil { return nil, err } added := &e.resources[len(e.resources)-1] added.vectorKey, added.vectorStates = key, maps.Clone(states) } data, err := editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: e.nextID()}, {Name: xml.Name{Local: "Boundary"}, Value: editorBoxString(extent)}, {Name: xml.Name{Local: "ResourceID"}, Value: resource}}, nil) if err != nil { return nil, err } return newEditorCompositeNode(data) } // UngroupCompositeObject 展开内部组合,隔离引用资源并保留父容器的样式和裁剪 // 入参: page 页面索引, path 父路径, index 组合序号 // 返回: []int 展开后的成员序号, error 错误信息 func (e *Editor) UngroupCompositeObject(page int, path ObjectPath, index int) ([]int, error) { var result []int err := e.editCompositeObjects(page, path, []int{index}, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error { node := nodes[0] if !members[0].Capabilities.Order { return fmt.Errorf("composite container cannot be reordered") } return e.ungroupCompositeNode(renderer, node, func(objects []GraphicObject) error { var content []byte for i, object := range objects { content = append(content, object.origin.data...) maps.Copy(node.owner.states, object.CompositeGraphicUnit.states) node.owner.states[editorObjectID(object)] = object.state result = append(result, index+i) } delete(node.owner.states, editorObjectID(node.object)) node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, content}) return nil }) }) return result, err } // ungroupCompositeNode 将组合展开至所在坐标系,祖先透明度和裁剪仅由原父级应用一次 // 入参: renderer 当前读取器, node 待展开组合, replace 原子替换回调 // 返回: error 错误信息 func (e *Editor) ungroupCompositeNode(renderer *Renderer, node *editorCompositeNode, replace func([]GraphicObject) error) error { if !e.compositeUngroupable(node.node) { return fmt.Errorf("group container cannot be removed without losing attributes") } copy, err := newEditorCompositeNode(node.data) if err != nil { return err } copy.setStates(node.states) if !node.boundaryInCTM { if err := copy.convertCoordinates(node.parent, true); err != nil { return err } } copy.parent, copy.boundaryInCTM, copy.visible = IdentityMatrix, true, true copy.defaults, copy.drawParams = node.defaults, node.drawParams members, err := e.compositeMembers(copy, renderer.Reader, renderer, make(map[string]bool)) if err != nil { return err } if len(members) == 0 { return fmt.Errorf("group has no objects") } for _, member := range members { if !member.transformable() || !editorXMLCopyable(member.node) { return fmt.Errorf("group member cannot be expanded") } } return e.pasteCompositeSelection(&CompositeSelection{editor: e, document: e.documentIdentity(), nodes: members}, func(objects []GraphicObject) error { ids := make(map[string]string) if copy.ref != nil { for _, object := range objects { if err := e.collectCompositeIDs(object.origin.node, ids); err != nil { return err } } } for i, object := range objects { data := object.origin.data if len(ids) != 0 { data, err = editorXMLRemapIDs(data, object.origin.node, ids) if err != nil { return err } } member, err := newEditorCompositeNode(data) if err != nil { return err } states := maps.Clone(object.CompositeGraphicUnit.states) if states == nil { states = make(map[string]editorCompositeState) } states[editorObjectID(object)] = object.state if len(ids) != 0 { states = remapCompositeStates(states, ids) } if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" { if err := member.convertCoordinates(IdentityMatrix, true); err != nil { return err } } if !node.boundaryInCTM { if err := member.convertCoordinates(node.parent, false); err != nil { return err } } member.node.parent = &editorXML{name: xml.Name{Local: "Layer"}, parent: &editorXML{name: xml.Name{Local: "Content"}}} member.object.state = states[editorObjectID(member.object)] member.object.TextObject.layout = member.object.state.layout member.object.CompositeGraphicUnit.states = states member.object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), data: member.data, node: member.node, object: member.object} objects[i] = member.object } return replace(objects) }) } // compositeUngroupable 检查待移除的容器及引用链,子对象原文不展开重写 // 入参: node 组合节点 // 返回: bool 是否可解组 func (e *Editor) compositeUngroupable(node *editorXML) bool { seen := make(map[string]bool) for { if !editorUngroupable(node) { return false } id := node.attr("ResourceID") if id == "" { return true } key := id if value := editorResourceID(id); value != "" { key = value } if seen[key] { return false } seen[key] = true resource, err := e.compositeDefinition(id) if err != nil { return false } node = resource.node } } // compositeDefinition 读取复合资源原文,不加载图片或字体数据 // 入参: id 资源标识 // 返回: *editorCompositeNode 定义节点, error 错误信息 func (e *Editor) compositeDefinition(id string) (*editorCompositeNode, error) { var data []byte for _, resource := range e.resources { if sameResourceID(resource.composite, id) { data = resource.data break } } if data == nil && e.source != nil { var err error name, _ := resourceValue(e.source.reader.resourceFiles, id) data, err = e.source.reader.readFile(name) if err != nil { return nil, err } } root, err := parseEditorXML(data) if err != nil { return nil, err } if unit := editorResourceNode(root, "CompositeGraphicUnits", "CompositeGraphicUnit", id); unit != nil { fragment, err := editorXMLStandalone(data[unit.start:unit.end], unit) if err != nil { return nil, err } return newEditorCompositeNode(fragment) } return nil, fmt.Errorf("composite resource %q not found", id) }