mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
264 lines
9.2 KiB
Go
264 lines
9.2 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"
|
|
|
|
"encoding/xml"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strconv"
|
|
)
|
|
|
|
// GroupObjects 将同一容器内连续的对象组合,保留标识、绘制顺序及原始内容
|
|
// 入参: page 页面索引, ids 至少两个对象标识
|
|
// 返回: string 组合标识, error 错误信息
|
|
func (e *Editor) GroupObjects(page int, ids []string) (id string, err error) {
|
|
_, indexes, err := e.selectedObjects(page, ids)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(indexes) < 2 {
|
|
return "", fmt.Errorf("grouping requires at least two objects")
|
|
}
|
|
slices.SortFunc(indexes, compareEditorPosition)
|
|
layer := &e.pages[page].Content.Layer[indexes[0].layer]
|
|
parent, siblings := e.objectOrderIndexes(page, layer, editorObjectID(layer.Objects[indexes[0].index]))
|
|
first := slices.Index(siblings, indexes[0].index)
|
|
for i, index := range indexes {
|
|
id := editorObjectID(e.pages[page].Content.Layer[index.layer].Objects[index.index])
|
|
capability, err := e.ObjectCapabilities(page, id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if index.layer != indexes[0].layer || first+i >= len(siblings) || siblings[first+i] != index.index || !capability.Copy || !capability.Order {
|
|
return "", fmt.Errorf("grouping requires consecutive objects in the same container")
|
|
}
|
|
}
|
|
reader, err := e.reader(nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer reader.Close()
|
|
renderer := e.newRenderer(reader)
|
|
var extent Box
|
|
for _, index := range indexes {
|
|
object := layer.Objects[index.index]
|
|
boundary, _ := editorGeometry(object)
|
|
box, err := creationBox(boundary)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
visible, err := renderer.ObjectBounds(object, layer.DrawParam)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
extent = unionTextBox(extent, unionTextBox(box, visible))
|
|
}
|
|
var content []byte
|
|
var source *editorSourcePage
|
|
states := make(map[string]editorCompositeState)
|
|
for _, index := range indexes {
|
|
before := layer.Objects[index.index]
|
|
object := transformEditorMatrix(cloneEditorData(before), TranslationMatrix(-extent.X, -extent.Y))
|
|
var data []byte
|
|
if origin := e.objectOrigin(editorObjectID(object)); origin != nil {
|
|
source = origin.page
|
|
data, err = editorXMLObject(origin.data, origin.node, origin.object, object)
|
|
if err == nil {
|
|
data, err = editorXMLStandalone(data, origin.node)
|
|
}
|
|
} else {
|
|
data, err = editorObjectXML(object)
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
|
maps.Copy(states, object.CompositeGraphicUnit.states)
|
|
state := object.state
|
|
if object.Type == "TextObject" && object.TextObject.layout != nil {
|
|
state.layout = object.TextObject.layout
|
|
state.origin[0], _, err = e.richPlainOffset(object.TextObject)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
state.origin[1], _ = strconv.ParseFloat(object.TextObject.TextCode[0].Y, 64)
|
|
}
|
|
states[editorObjectID(object)] = state
|
|
}
|
|
if err := e.prepareSourceIDs(); err != nil {
|
|
return "", err
|
|
}
|
|
maxID, resourceCount := e.maxID, len(e.resources)
|
|
defer func() {
|
|
if err != nil {
|
|
e.maxID, e.resources = maxID, e.resources[:resourceCount]
|
|
}
|
|
}()
|
|
id, resourceID := e.nextID(), e.nextID()
|
|
content, err = editorXMLContainer("Content", nil, content)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
data, err := editorXMLContainer("CompositeGraphicUnit", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: resourceID}, {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 "", err
|
|
}
|
|
data, err = editorXMLContainer("CompositeGraphicUnits", nil, bytes.TrimPrefix(data, []byte(xml.Header)))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
data, err = editorXMLContainer("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}, bytes.TrimPrefix(data, []byte(xml.Header)))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resource, err := e.compositeResource(resourceID, data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resource.states = states
|
|
e.resources = append(e.resources, resource)
|
|
data, err = editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Boundary"}, Value: editorBoxString(extent)}, {Name: xml.Name{Local: "ResourceID"}, Value: resourceID}}, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
group, err := newEditorCompositeNode(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if parent == nil {
|
|
parent = &editorXML{name: xml.Name{Local: "Layer"}, attrs: []xml.Attr{{Name: xml.Name{Local: "ID"}, Value: layer.ID}}, parent: &editorXML{name: xml.Name{Local: "Content"}}}
|
|
}
|
|
group.node.parent = parent
|
|
group.object.CompositeGraphicUnit.states = states
|
|
origin := &editorObjectOrigin{page: source, data: data, node: group.node, object: group.object}
|
|
e.replaceGroup(page, indexes, []GraphicObject{group.object}, map[string]*editorObjectOrigin{id: origin})
|
|
return id, nil
|
|
}
|
|
|
|
// UngroupObject 展开组合,保留页面外观与裁剪,资源引用分配独立标识,含容器行为或扩展的组合不拆解
|
|
// 入参: page 页面索引, id 组合标识
|
|
// 返回: []string 展开后的对象标识, error 错误信息
|
|
func (e *Editor) UngroupObject(page int, id string) ([]string, error) {
|
|
_, indexes, err := e.selectedObjects(page, []string{id})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
origin := e.objectOrigin(id)
|
|
if origin == nil || !editorContainerOrderable(origin.node.parent) {
|
|
return nil, fmt.Errorf("group container cannot be removed without losing attributes")
|
|
}
|
|
reader, renderer, root, _, err := e.compositeScope(page, ObjectPath{ID: id})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer reader.Close()
|
|
var result []string
|
|
err = e.ungroupCompositeNode(renderer, root, func(objects []GraphicObject, remaps []map[string]string) error {
|
|
origins := make(map[string]*editorObjectOrigin, len(objects))
|
|
for i := range objects {
|
|
object := &objects[i]
|
|
copy := *object.origin
|
|
copy.node.parent = origin.node.parent
|
|
object.origin = nil
|
|
id := editorObjectID(*object)
|
|
origins[id] = ©
|
|
result = append(result, id)
|
|
}
|
|
return e.remapStructureObjects(page, remaps, func() error {
|
|
e.replaceGroup(page, indexes, objects, origins)
|
|
return nil
|
|
})
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
// editorUngroupable 判断移除容器是否会丢失对象行为或未知属性
|
|
// 入参: node 容器节点
|
|
// 返回: bool 是否可解组
|
|
func editorUngroupable(node *editorXML) bool {
|
|
if node.name.Local != "CompositeObject" && node.name.Local != "CompositeGraphicUnit" {
|
|
return false
|
|
}
|
|
if !editorXMLAttributes(node, "ID Boundary CTM DrawParam Alpha Visible ResourceID Width Height") {
|
|
return false
|
|
}
|
|
var contentSupported func(*editorXML) bool
|
|
contentSupported = func(content *editorXML) bool {
|
|
attributes := ""
|
|
if content.name.Local == "PageBlock" {
|
|
attributes = "ID"
|
|
}
|
|
if !editorXMLAttributes(content, attributes) {
|
|
return false
|
|
}
|
|
for _, member := range content.children {
|
|
if classifyOFDNamespace(member.name.Space) == ofdXMLUnknown {
|
|
return false
|
|
}
|
|
if member.name.Local == "PageBlock" {
|
|
if !contentSupported(member) {
|
|
return false
|
|
}
|
|
} else if !slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject", "CompositeGraphicUnit"}, member.name.Local) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
for _, child := range node.children {
|
|
switch child.name.Local {
|
|
case "Content", "PageBlock":
|
|
if !contentSupported(child) {
|
|
return false
|
|
}
|
|
case "Clips":
|
|
if !editorXMLSupported(child) {
|
|
return false
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// replaceGroup 原子替换同容器连续成员及原文来源,提交一次撤销记录
|
|
// 入参: page 页面索引, indexes 原对象位置, objects 新成员, origins 新成员原文
|
|
func (e *Editor) replaceGroup(page int, indexes []editorObjectPosition, objects []GraphicObject, origins map[string]*editorObjectOrigin) {
|
|
before := copyEditorPage(e.pages[page]).Content.Layer
|
|
after := copyEditorPage(e.pages[page]).Content.Layer
|
|
layer := &after[indexes[0].layer]
|
|
layer.Objects = slices.Replace(layer.Objects, indexes[0].index, indexes[len(indexes)-1].index+1, objects...)
|
|
previous := make(map[string]*editorObjectOrigin, len(origins))
|
|
for id := range origins {
|
|
previous[id] = e.objectOrigin(id)
|
|
}
|
|
apply := func(e *Editor, layers []Layer, origins map[string]*editorObjectOrigin) {
|
|
e.pages[page].Content.Layer = copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer
|
|
for id, origin := range origins {
|
|
e.setObjectOrigin(id, origin)
|
|
}
|
|
}
|
|
apply(e, after, origins)
|
|
if change := e.recordChange(page); change != nil {
|
|
change.undo = func(e *Editor) { apply(e, before, previous) }
|
|
change.redo = func(e *Editor) { apply(e, after, origins) }
|
|
}
|
|
}
|