Files
ofdgo/ofdgo_editor_composite_group.go
T
2026-10-02 13:08:34 +08:00

314 lines
11 KiB
Go

// 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, 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, 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
}
if seen[id] {
return false
}
seen[id] = 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 resource.composite == id {
data = resource.data
break
}
}
if data == nil && e.source != nil {
var err error
data, err = e.source.reader.readFile(e.source.reader.resourceFiles[id])
if err != nil {
return nil, err
}
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
if units := root.child("CompositeGraphicUnits"); units != nil {
for _, unit := range units.children {
if unit.name.Local == "CompositeGraphicUnit" && unit.attr("ID") == id {
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)
}