mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
314 lines
11 KiB
Go
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)
|
|
}
|