mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
501 lines
17 KiB
Go
501 lines
17 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"
|
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
|
"maps"
|
|
"slices"
|
|
)
|
|
|
|
// CompositeSelection 同一内部容器的独立选区快照,共享不可变字体和图片资源
|
|
// 可在当前Editor内粘贴,源对象修改或删除不改变快照,不用于跨文档交换
|
|
// 父路径的祖先成员增删或排序后需重新捕获,避免使用已失效的序号路径
|
|
type CompositeSelection struct {
|
|
editor *Editor
|
|
document editorDocumentIdentity
|
|
page string
|
|
path ObjectPath
|
|
references int
|
|
containers []int
|
|
nodes []*editorCompositeNode
|
|
source *editorSourcePage
|
|
}
|
|
|
|
// PasteCompositeSelection 将内部对象快照粘贴到页面顶层,保持嵌套结构、源继承外观和裁剪
|
|
// 入参: page 目标页, selection 当前编辑器内捕获的快照, dx 页面横向位移, dy 页面纵向位移
|
|
// 返回: []string 新对象标识, error 错误信息
|
|
func (e *Editor) PasteCompositeSelection(page int, selection *CompositeSelection, dx, dy float64) ([]string, error) {
|
|
if !finite(dx) || !finite(dy) {
|
|
return nil, fmt.Errorf("paste requires finite offsets")
|
|
}
|
|
if _, err := e.page(page); err != nil {
|
|
return nil, err
|
|
}
|
|
var result []string
|
|
err := e.pasteCompositeSelection(selection, func(objects []GraphicObject, _ []map[string]string) error {
|
|
var err error
|
|
result, err = e.CopyObjects(page, objects, dx, dy)
|
|
return err
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
// CaptureCompositeObjects 捕获内部选区,不修改文档或分配资源
|
|
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号
|
|
// 返回: *CompositeSelection 独立快照, error 错误信息
|
|
func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []int) (*CompositeSelection, error) {
|
|
reader, _, root, nodes, err := e.compositeScope(page, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer reader.Close()
|
|
if len(indexes) == 0 {
|
|
return nil, fmt.Errorf("composite selection is empty")
|
|
}
|
|
indexes = slices.Clone(indexes)
|
|
slices.Sort(indexes)
|
|
var owner *editorCompositeNode
|
|
var container *editorXML
|
|
selection := &CompositeSelection{editor: e, document: e.documentIdentity(), page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: slices.Clone(path.Children)}}
|
|
if path.Annotation == "" {
|
|
selection.source = e.objectOrigin(path.ID).page
|
|
} else if e.source != nil {
|
|
selection.source = e.source.pages[e.pages[page].ID]
|
|
}
|
|
for j, i := range indexes {
|
|
if i < 0 || i >= len(nodes) || j > 0 && indexes[j-1] == i {
|
|
return nil, fmt.Errorf("invalid composite copy selection")
|
|
}
|
|
node := nodes[i]
|
|
if !node.transformable() || !editorXMLCopyable(node.node) {
|
|
return nil, fmt.Errorf("composite member cannot be copied")
|
|
}
|
|
if j == 0 {
|
|
owner, container = node.owner, node.span.parent
|
|
} else if node.span.parent != container {
|
|
return nil, fmt.Errorf("copy requires members in the same container")
|
|
}
|
|
copy := *node
|
|
copy.owner, copy.span = nil, nil
|
|
selection.nodes = append(selection.nodes, ©)
|
|
}
|
|
scope := root.loadedScope(path.Children)
|
|
for scope != owner {
|
|
scope = scope.ref
|
|
selection.references++
|
|
}
|
|
for node := container; node != owner.node; node = node.parent {
|
|
containers := compositeContainers(node.parent)
|
|
selection.containers = append(selection.containers, slices.Index(containers, node))
|
|
}
|
|
slices.Reverse(selection.containers)
|
|
return selection, nil
|
|
}
|
|
|
|
// PasteCompositeObjects 将快照粘贴至内部范围末尾,不依赖源成员当前的序号或存在性
|
|
// 同范围沿用原容器和当前继承样式,跨范围固定对象的源外观,位移使用页面毫米
|
|
// 目标范围的父透明度及裁剪仍作用于新成员,全部成员提交一次撤销记录
|
|
// 入参: page 页面索引, path 父复合路径, selection 捕获的快照, dx 页面横向位移, dy 页面纵向位移
|
|
// 返回: []int 新成员序号, error 错误信息
|
|
func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *CompositeSelection, dx, dy float64) ([]int, error) {
|
|
if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) {
|
|
return nil, fmt.Errorf("composite selection belongs to another document or editor")
|
|
}
|
|
if !finite(dx) || !finite(dy) {
|
|
return nil, fmt.Errorf("paste requires finite offsets")
|
|
}
|
|
if _, err := e.page(page); err != nil {
|
|
return nil, err
|
|
}
|
|
if e.pages[page].ID != selection.page || path.ID != selection.path.ID || path.Annotation != selection.path.Annotation || !slices.Equal(path.Children, selection.path.Children) {
|
|
var result []int
|
|
err := e.pasteCompositeSelection(selection, func(objects []GraphicObject, _ []map[string]string) error {
|
|
var err error
|
|
result, err = e.CopyObjectsToComposite(page, path, objects, dx, dy)
|
|
return err
|
|
})
|
|
return result, err
|
|
}
|
|
var result []int
|
|
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, _ []*editorCompositeNode) error {
|
|
scope := root.loadedScope(path.Children)
|
|
owner := scope
|
|
for i := 0; i < selection.references; i++ {
|
|
owner = owner.ref
|
|
if owner == nil {
|
|
return fmt.Errorf("composite resource container no longer exists")
|
|
}
|
|
}
|
|
container := owner.node
|
|
for _, index := range selection.containers {
|
|
containers := compositeContainers(container)
|
|
if index >= len(containers) {
|
|
return fmt.Errorf("composite page block no longer exists")
|
|
}
|
|
container = containers[index]
|
|
}
|
|
box, _ := ParseBox(owner.object.CompositeGraphicUnit.Boundary)
|
|
parent := owner.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(owner.object.CompositeGraphicUnit.CTM))
|
|
nodes := make([]*editorCompositeNode, len(selection.nodes))
|
|
for i, node := range selection.nodes {
|
|
copy := *node
|
|
copy.parent, copy.boundaryInCTM = parent, owner.boundaryInCTM
|
|
nodes[i] = ©
|
|
}
|
|
data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
maps.Copy(owner.states, states)
|
|
next, reached := 0, false
|
|
var count func(*editorCompositeNode)
|
|
count = func(node *editorCompositeNode) {
|
|
if node.ref != nil {
|
|
count(node.ref)
|
|
}
|
|
if reached {
|
|
return
|
|
}
|
|
for _, child := range node.children {
|
|
if node != owner || child.span.start < container.close {
|
|
next++
|
|
}
|
|
}
|
|
reached = node == owner
|
|
}
|
|
count(scope)
|
|
patch := editorXMLPatch{container.close, container.close, data}
|
|
if container.open == container.end {
|
|
patch = editorXMLContent(owner.data, container, data)
|
|
}
|
|
owner.patches = append(owner.patches, patch)
|
|
for i := range nodes {
|
|
result = append(result, next+i)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// pasteCompositeSelection 固定内部快照的页面几何和外观,粘贴失败时回收新增绘制资源
|
|
// 入参: selection 独立内部快照, paste 目标范围粘贴操作
|
|
// 返回: error 错误信息
|
|
func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste func([]GraphicObject, []map[string]string) error) (err error) {
|
|
if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) {
|
|
return fmt.Errorf("composite selection belongs to another document or editor")
|
|
}
|
|
count, maximum := len(e.resources), e.maxID
|
|
ready := e.source != nil && e.source.idsReady
|
|
defer func() {
|
|
if err != nil {
|
|
clear(e.resources[count:])
|
|
e.resources, e.maxID = e.resources[:count], maximum
|
|
if e.source != nil {
|
|
e.source.idsReady = ready
|
|
}
|
|
}
|
|
}()
|
|
if err := e.prepareSourceIDs(); err != nil {
|
|
return err
|
|
}
|
|
reader, err := e.reader(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer reader.Close()
|
|
renderer := e.newRenderer(reader)
|
|
var objects []GraphicObject
|
|
var remaps []map[string]string
|
|
for _, source := range selection.nodes {
|
|
copy, err := newEditorCompositeNode(source.data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
style, styleErr := e.compositeMemberStyle(source)
|
|
if styleErr != nil {
|
|
if source.object.Type != "PathObject" || editReason(styleErr) != EditUnsupportedColor {
|
|
return styleErr
|
|
}
|
|
copy.setStates(source.states)
|
|
copy.defaults = source.defaults
|
|
if !source.boundaryInCTM {
|
|
if err := copy.convertCoordinates(source.parent, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
copy, err = e.wrapCompositePath(renderer, copy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
copy.setStates(source.states)
|
|
copy.defaults = source.defaults
|
|
copy.drawParams = source.drawParams
|
|
object := cloneEditorData(copy.object)
|
|
if styleErr == nil && object.Type != "PathObject" {
|
|
object = style
|
|
}
|
|
object = mergeGraphicObjectAlpha(object, source.alpha)
|
|
if !source.visible || source.clip != nil && len(*source.clip) == 0 {
|
|
visible := false
|
|
switch object.Type {
|
|
case "TextObject":
|
|
object.TextObject.Visible = &visible
|
|
case "PathObject":
|
|
object.PathObject.Visible = &visible
|
|
case "ImageObject":
|
|
object.ImageObject.Visible = &visible
|
|
case "CompositeObject", "CompositeGraphicUnit":
|
|
object.CompositeGraphicUnit.Visible = &visible
|
|
}
|
|
}
|
|
if err := copy.update(object); err != nil {
|
|
return err
|
|
}
|
|
if !source.boundaryInCTM && styleErr == nil {
|
|
if err := copy.convertCoordinates(source.parent, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
preserved, err := copy.preserveActionRegions(source.parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !preserved {
|
|
wrapped, err := e.vectorInstance(renderer, []*editorCompositeNode{copy})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wrapped.setStates(copy.states)
|
|
wrapped.defaults, wrapped.drawParams = copy.defaults, copy.drawParams
|
|
copy = wrapped
|
|
}
|
|
copy.parent, copy.boundaryInCTM = IdentityMatrix, true
|
|
if err := e.transformCompositeMember(renderer, copy, source.parent); err != nil {
|
|
return err
|
|
}
|
|
if err := e.isolateCompositeStyle(copy); err != nil {
|
|
return err
|
|
}
|
|
if source.clip != nil && len(*source.clip) != 0 {
|
|
shape, err := geometryClipPath(*source.clip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clips := *editorObjectClips(©.object)
|
|
matrix, ok := copy.matrix(clips.usesObjectMatrix()).Invert()
|
|
if !ok {
|
|
return fmt.Errorf("composite clip transform is not invertible")
|
|
}
|
|
if err := appendCompositeClip(copy, shape, matrix, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if copy.object.Type == "CompositeObject" || copy.object.Type == "CompositeGraphicUnit" {
|
|
if err := copy.convertCoordinates(IdentityMatrix, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
copy.node.parent = &editorXML{name: xml.Name{Local: "Layer"}, parent: &editorXML{name: xml.Name{Local: "Content"}}}
|
|
object, err = e.copiedObjectStyle(copy.object, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
object.state = copy.states[editorObjectID(copy.object)]
|
|
object.TextObject.layout = object.state.layout
|
|
copy.object.TextObject.layout = object.TextObject.layout
|
|
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
|
object.CompositeGraphicUnit.states = maps.Clone(copy.states)
|
|
copy.object.CompositeGraphicUnit.states = object.CompositeGraphicUnit.states
|
|
}
|
|
object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), page: selection.source, data: copy.data, node: copy.node, object: copy.object}
|
|
objects = append(objects, object)
|
|
remaps = append(remaps, copy.remaps...)
|
|
}
|
|
return paste(objects, remaps)
|
|
}
|
|
|
|
// preserveActionRegions 将默认点击范围固定到对象坐标,保留动作原文及显式区域
|
|
// 入参: matrix 即将合入对象的父级变换
|
|
// 返回: bool 是否可直接表示,否则需保留容器, error 错误信息
|
|
func (n *editorCompositeNode) preserveActionRegions(matrix Matrix) (bool, error) {
|
|
actions := n.node.child("Actions")
|
|
if actions == nil {
|
|
return true, nil
|
|
}
|
|
boundary, ctm := editorGeometry(n.object)
|
|
if n.object.Type == "ImageObject" && ctm == "" && (matrix.a != 1 || matrix.b != 0 || matrix.c != 0 || matrix.d != 1) {
|
|
return false, nil
|
|
}
|
|
if basegeom.AxisAligned(basegeom.MatrixFromValues(matrix.Values())) {
|
|
return true, nil
|
|
}
|
|
var targets []*editorXML
|
|
for _, action := range actions.children {
|
|
if !action.matchesOFD("Action") {
|
|
continue
|
|
}
|
|
if action.attr("Event") == "CLICK" && action.child("Region") == nil {
|
|
targets = append(targets, action)
|
|
}
|
|
}
|
|
if len(targets) == 0 {
|
|
return true, nil
|
|
}
|
|
box, err := creationBox(boundary)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
inverse, ok := NewMatrix(ctm).Invert()
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
points := [4]Point{{}, {X: box.W}, {X: box.W, Y: box.H}, {Y: box.H}}
|
|
var values [4]string
|
|
for i, point := range points {
|
|
x, y := inverse.Transform(point.X, point.Y)
|
|
if !finite(x) || !finite(y) {
|
|
return false, nil
|
|
}
|
|
values[i] = ofdNumber(x) + " " + ofdNumber(y)
|
|
}
|
|
region, err := encodeOFDXML(func(x *ofdXML) {
|
|
x.root("Region", nil)
|
|
x.start("Area", ofdAttrs{{Name: xml.Name{Local: "Start"}, Value: values[0]}})
|
|
for _, point := range values[1:] {
|
|
x.start("Line", ofdAttrs{{Name: xml.Name{Local: "Point1"}, Value: point}})
|
|
x.end("Line")
|
|
}
|
|
x.start("Close", nil)
|
|
x.end("Close")
|
|
x.end("Area")
|
|
x.end("Region")
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
region = bytes.TrimPrefix(region, []byte(xml.Header))
|
|
patches := make([]editorXMLPatch, 0, len(targets))
|
|
for _, target := range targets {
|
|
patch := editorXMLPatch{target.open, target.open, region}
|
|
if target.open == target.end {
|
|
patch = editorXMLContent(n.data, target, region)
|
|
}
|
|
patches = append(patches, patch)
|
|
}
|
|
data := editorPatchXML(n.data, patches)
|
|
next, err := newEditorCompositeNode(data)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
n.data, n.node, n.object, n.changed = data, next.node, next.object, true
|
|
return true, nil
|
|
}
|
|
|
|
// convertCoordinates 转换内联基本成员的边界约定,引用资源保持标准局部坐标
|
|
// 入参: parent 所在范围矩阵, inward 是否转换到随父变换的坐标
|
|
// 返回: error 错误信息
|
|
func (n *editorCompositeNode) convertCoordinates(parent Matrix, inward bool) error {
|
|
if n.object.Type != "CompositeObject" && n.object.Type != "CompositeGraphicUnit" {
|
|
return n.update(compositeBoundary(n.object, parent, inward))
|
|
}
|
|
var patches []editorXMLPatch
|
|
var visit func(*editorXML, Matrix) error
|
|
visit = func(node *editorXML, matrix Matrix) error {
|
|
if node.name.Local == "CompositeObject" || node.name.Local == "CompositeGraphicUnit" {
|
|
box, _ := ParseBox(node.attr("Boundary"))
|
|
matrix = matrix.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(node.attr("CTM")))
|
|
if _, ok := matrix.Invert(); !ok {
|
|
return fmt.Errorf("composite copy requires invertible transforms")
|
|
}
|
|
}
|
|
for _, child := range node.children {
|
|
if classifyOFDNamespace(child.name.Space) == ofdXMLUnknown {
|
|
continue
|
|
}
|
|
switch child.name.Local {
|
|
case "Content", "PageBlock", "CompositeObject", "CompositeGraphicUnit":
|
|
if err := visit(child, matrix); err != nil {
|
|
return err
|
|
}
|
|
case "TextObject", "PathObject", "ImageObject":
|
|
data, err := editorXMLStandalone(n.data[child.start:child.end], child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
member, err := newEditorCompositeNode(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := member.update(compositeBoundary(cloneEditorData(member.object), matrix, inward)); err != nil {
|
|
return err
|
|
}
|
|
patches = append(patches, editorXMLPatch{child.start, child.end, member.data})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := visit(n.node, parent); err != nil {
|
|
return err
|
|
}
|
|
data := editorPatchXML(n.data, patches)
|
|
next, err := newEditorCompositeNode(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.data, n.node, n.object = data, next.node, next.object
|
|
return nil
|
|
}
|
|
|
|
// loadedScope 获取已由compositeScope展开并校验的范围
|
|
// 入参: path 逐层成员序号
|
|
// 返回: *editorCompositeNode 范围节点
|
|
func (n *editorCompositeNode) loadedScope(path []int) *editorCompositeNode {
|
|
for _, index := range path {
|
|
var nodes []*editorCompositeNode
|
|
var collect func(*editorCompositeNode)
|
|
collect = func(node *editorCompositeNode) {
|
|
if node.ref != nil {
|
|
collect(node.ref)
|
|
}
|
|
nodes = append(nodes, node.children...)
|
|
}
|
|
collect(n)
|
|
n = nodes[index]
|
|
}
|
|
return n
|
|
}
|
|
|
|
// compositeContainers 获取直接内容容器,序号不随绘制成员增删变化
|
|
// 入参: node 父节点
|
|
// 返回: []*editorXML 内容容器与页块
|
|
func compositeContainers(node *editorXML) []*editorXML {
|
|
var result []*editorXML
|
|
for _, child := range node.children {
|
|
if (child.name.Local == "Content" || child.name.Local == "PageBlock") && classifyOFDNamespace(child.name.Space) != ofdXMLUnknown {
|
|
result = append(result, child)
|
|
}
|
|
}
|
|
return result
|
|
}
|