mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
463 lines
17 KiB
Go
463 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"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
|
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
|
)
|
|
|
|
// DeleteCompositeObjects 删除内部成员,保留容器、未选对象及其他实例,一次撤销恢复全部
|
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号
|
|
// 返回: error 错误信息
|
|
func (e *Editor) DeleteCompositeObjects(page int, path ObjectPath, indexes []int) error {
|
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error {
|
|
for _, node := range nodes {
|
|
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, nil})
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// CopyCompositeObjects 在原容器末尾复制选区并按页面毫米平移,保留原文并重映射副本内部标识
|
|
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号, dx 页面横向位移, dy 页面纵向位移
|
|
// 返回: []int 按绘制顺序排列的副本序号, error 错误信息
|
|
func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int, dx, dy float64) ([]int, error) {
|
|
if !finite(dx) || !finite(dy) {
|
|
return nil, fmt.Errorf("copy requires finite offsets")
|
|
}
|
|
var result []int
|
|
metadata := make(map[int]richTextMetadata)
|
|
err := e.Transaction(func(e *Editor) error {
|
|
err := e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
|
container := nodes[0].span.parent
|
|
for i, node := range nodes {
|
|
if !members[i].Capabilities.Copy || node.span.parent != container {
|
|
return fmt.Errorf("copy requires editable members in the same container")
|
|
}
|
|
}
|
|
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
|
|
for i, node := range nodes {
|
|
if node.object.Type == "CompositeObject" {
|
|
entry, err := e.richTextEntry(page, editorObjectID(node.object))
|
|
if err == nil {
|
|
metadata[i] = entry.metadata
|
|
} else if !errors.Is(err, ErrNotRichText) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
owner := nodes[0].owner
|
|
maps.Copy(owner.states, states)
|
|
owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, data})
|
|
next := 0
|
|
for _, member := range owner.children {
|
|
if container.start < member.span.start && member.span.end <= container.close {
|
|
next = member.index + 1
|
|
}
|
|
}
|
|
for i := range nodes {
|
|
result = append(result, next+i)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return e.putCompositeRichTextCopies(page, path, result, metadata)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例
|
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, order 为up、down、top或bottom
|
|
// 返回: []int 调整后的选区序号, error 错误信息
|
|
func (e *Editor) OrderCompositeObjects(page int, path ObjectPath, indexes []int, order string) ([]int, error) {
|
|
if !slices.Contains([]string{"up", "down", "top", "bottom"}, order) {
|
|
return nil, fmt.Errorf("invalid object order %q", order)
|
|
}
|
|
var result []int
|
|
err := e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
|
siblings := nodes[0].siblings()
|
|
selected := make([]bool, len(siblings))
|
|
for j, node := range nodes {
|
|
i := slices.Index(siblings, node)
|
|
if i < 0 || !members[j].Capabilities.Order {
|
|
return fmt.Errorf("ordering requires members in the same container")
|
|
}
|
|
selected[i] = true
|
|
}
|
|
ordered := slices.Clone(siblings)
|
|
orderEditorObjects(ordered, selected, order)
|
|
for i, node := range siblings {
|
|
if node != ordered[i] {
|
|
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, ordered[i].data})
|
|
}
|
|
if slices.Contains(nodes, ordered[i]) {
|
|
result = append(result, node.index)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// ReplaceCompositeImage 替换内部图片数据,保留变换、裁剪、蒙版、边框和透明度
|
|
// 更换主图资源时清除旧替换图引用,不修改其他共享实例
|
|
// 入参: page 页面索引, path 父复合路径, index 图片序号, data AddImage支持的图片数据
|
|
// 返回: error 错误信息
|
|
func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, data []byte) error {
|
|
return e.editCompositeObjects(page, path, []int{index}, func(renderer *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error {
|
|
node := nodes[0]
|
|
if node.object.Type != "ImageObject" {
|
|
return fmt.Errorf("composite member is not an image")
|
|
}
|
|
object := node.object
|
|
decoded, _, err := baseimage.DecodePixelsContext(context.Background(), data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if object.ImageObject.ImageMask != "" {
|
|
mask, err := renderer.ImageResource(object.ImageObject.ImageMask)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validateImageMask(context.Background(), mask, decoded.Bounds().Size()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
id, err := e.addDecodedImage(data, decoded)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !sameResourceID(object.ImageObject.ResourceID, id) {
|
|
object.ImageObject.Substitution = ""
|
|
}
|
|
object.ImageObject.ResourceID = id
|
|
return node.update(object)
|
|
})
|
|
}
|
|
|
|
// Shape 获取内部基本路径的页面几何,矩形和椭圆要求轴对齐,其他成员返回空类型
|
|
// 返回: ShapeKind 图形类型, Box 几何范围,直线使用起点及有符号端点位移
|
|
func (m CompositeMember) Shape() (ShapeKind, Box) {
|
|
if m.Object.Type != "PathObject" || !m.Capabilities.Transform {
|
|
return "", Box{}
|
|
}
|
|
kind, box := m.Object.PathObject.Shape()
|
|
if kind == "" {
|
|
return "", Box{}
|
|
}
|
|
parent, ok := m.shapeParent()
|
|
if !ok {
|
|
return "", Box{}
|
|
}
|
|
if lineShapeKind(kind) {
|
|
return kind, transformLineBox(box, parent)
|
|
}
|
|
if !basegeom.AxisAligned(basegeom.MatrixFromValues(parent.Values())) {
|
|
return "", Box{}
|
|
}
|
|
return kind, parent.TransformBox(box)
|
|
}
|
|
|
|
// ShapeFrame 获取内部矩形或椭圆的局部范围及页面变换
|
|
// 返回: ShapeKind 图形类型, Box 局部范围, Matrix 页面变换
|
|
func (m CompositeMember) ShapeFrame() (ShapeKind, Box, Matrix) {
|
|
if m.Object.Type != "PathObject" || !m.Capabilities.Transform {
|
|
return "", Box{}, Matrix{}
|
|
}
|
|
kind, box, _ := m.Object.PathObject.ShapeFrame()
|
|
if _, ok := m.Matrix.Invert(); !ok {
|
|
return "", Box{}, Matrix{}
|
|
}
|
|
return kind, box, m.Matrix
|
|
}
|
|
|
|
// ReshapeCompositeFrame 沿内部矩形或椭圆自身方向拉伸,不修改共享实例
|
|
// 入参: page 页面索引, path 父路径, index 成员序号, box ShapeFrame局部目标范围
|
|
// 返回: error 错误信息
|
|
func (e *Editor) ReshapeCompositeFrame(page int, path ObjectPath, index int, box Box) error {
|
|
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
|
kind, previous, _ := members[0].ShapeFrame()
|
|
if kind == "" {
|
|
return fmt.Errorf("composite member is not a framed shape")
|
|
}
|
|
if box == previous {
|
|
return nil
|
|
}
|
|
node := nodes[0]
|
|
object := cloneEditorData(node.object)
|
|
if !node.boundaryInCTM {
|
|
object = compositeBoundary(object, node.parent, true)
|
|
}
|
|
var err error
|
|
object.PathObject, err = object.PathObject.ReshapeFrame(box)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !node.boundaryInCTM {
|
|
object = compositeBoundary(object, node.parent, false)
|
|
}
|
|
object.PathObject.Clips = node.object.PathObject.Clips
|
|
return node.update(object)
|
|
})
|
|
}
|
|
|
|
// ReshapeCompositeLine 修改内部直线的页面端点及箭头,保留原始属性和父变换
|
|
// 入参: page 页面索引, path 父路径, index 成员序号, kind 直线类型, box 页面端点
|
|
// 返回: error 错误信息
|
|
func (e *Editor) ReshapeCompositeLine(page int, path ObjectPath, index int, kind ShapeKind, box Box) error {
|
|
if kind != "" && !lineShapeKind(kind) {
|
|
return fmt.Errorf("unsupported line kind %q", kind)
|
|
}
|
|
return e.reshapeCompositeObject(page, path, index, kind, box, true)
|
|
}
|
|
|
|
// ReshapeCompositeObject 调整内部基本路径的页面几何,保留绘制样式、变换与裁剪
|
|
// 入参: page 页面索引, path 父路径, index 成员序号, box 页面范围,直线使用有符号端点位移
|
|
// 返回: error 错误信息
|
|
func (e *Editor) ReshapeCompositeObject(page int, path ObjectPath, index int, box Box) error {
|
|
return e.reshapeCompositeObject(page, path, index, "", box, false)
|
|
}
|
|
|
|
// siblings 获取同一原始容器内的直接绘制成员,不跨越资源和页块
|
|
// 返回: []*editorCompositeNode 按绘制顺序排列的成员
|
|
func (n *editorCompositeNode) siblings() []*editorCompositeNode {
|
|
var result []*editorCompositeNode
|
|
for _, member := range n.owner.children {
|
|
if member.span.parent == n.span.parent {
|
|
result = append(result, member)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// copyCompositeNodes 复制成员原文并统一重映射标识,保持副本之间的引用
|
|
// 入参: renderer 渲染器, nodes 快照, dx 页面横向位移, dy 页面纵向位移
|
|
// 返回: []byte 副本XML, map[string]editorCompositeState 会话信息, error 错误信息
|
|
func (e *Editor) copyCompositeNodes(renderer *Renderer, nodes []*editorCompositeNode, dx, dy float64) ([]byte, map[string]editorCompositeState, error) {
|
|
ids := make(map[string]string)
|
|
for _, node := range nodes {
|
|
if err := e.collectCompositeIDs(node.node, ids); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
var data []byte
|
|
states := make(map[string]editorCompositeState)
|
|
for _, node := range nodes {
|
|
copy := *node
|
|
if err := e.transformCompositeMember(renderer, ©, TranslationMatrix(dx, dy)); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
maps.Copy(states, remapCompositeStates(node.states, ids))
|
|
data = append(data, fragment...)
|
|
}
|
|
return data, states, nil
|
|
}
|
|
|
|
// update 合并成员属性变化,不重写原文中未修改的字段
|
|
// 入参: object 修改后的对象
|
|
// 返回: error 错误信息
|
|
func (n *editorCompositeNode) update(object GraphicObject) error {
|
|
if err := validateEditorGeometry(object); err != nil {
|
|
return err
|
|
}
|
|
data, err := editorXMLObject(n.data, n.node, n.object, object)
|
|
if err != nil || bytes.Equal(data, n.data) {
|
|
return err
|
|
}
|
|
next, err := newEditorCompositeNode(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.data, n.node, n.object, n.changed = data, next.node, next.object, true
|
|
return nil
|
|
}
|
|
|
|
// shapeParent 获取路径所在坐标到页面的变换,兼容旧式内联边界
|
|
// 返回: Matrix 路径父坐标变换, bool 是否可逆
|
|
func (m CompositeMember) shapeParent() (Matrix, bool) {
|
|
p := m.Object.PathObject
|
|
box, _ := ParseBox(p.Boundary)
|
|
inverse, ok := TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(p.CTM)).Invert()
|
|
return m.Matrix.Multiply(inverse), ok
|
|
}
|
|
|
|
// transformLineBox 变换起点及有符号端点位移
|
|
// 入参: box 直线端点, matrix 坐标变换
|
|
// 返回: Box 变换后的端点
|
|
func transformLineBox(box Box, matrix Matrix) Box {
|
|
x, y := matrix.Transform(box.X, box.Y)
|
|
w, h := basegeom.Vector(basegeom.MatrixFromValues(matrix.Values()), box.W, box.H)
|
|
return Box{X: x, Y: y, W: w, H: h}
|
|
}
|
|
|
|
// reshapeCompositeObject 按页面几何修改单个内部路径,可选修改箭头类型
|
|
// 入参: page 页面索引, path 父路径, index 成员序号, kind 目标类型,空值保持原类型, box 页面几何, lineOnly 是否仅接受直线
|
|
// 返回: error 错误信息
|
|
func (e *Editor) reshapeCompositeObject(page int, path ObjectPath, index int, kind ShapeKind, box Box, lineOnly bool) error {
|
|
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
|
before, geometry := members[0].Shape()
|
|
if before == "" || lineOnly && !lineShapeKind(before) {
|
|
return fmt.Errorf("composite member is not a supported shape")
|
|
}
|
|
if kind == "" {
|
|
kind = before
|
|
}
|
|
if kind == before && geometry == box {
|
|
return nil
|
|
}
|
|
node := nodes[0]
|
|
inverse, _ := node.parent.Invert()
|
|
object := cloneEditorData(node.object)
|
|
if !node.boundaryInCTM {
|
|
object = compositeBoundary(object, node.parent, true)
|
|
}
|
|
var err error
|
|
if lineShapeKind(kind) {
|
|
object.PathObject, err = object.PathObject.ReshapeLine(kind, transformLineBox(box, inverse))
|
|
} else {
|
|
if err := validateCreationBox(box); err != nil {
|
|
return err
|
|
}
|
|
object.PathObject, err = object.PathObject.Reshape(inverse.TransformBox(box))
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !node.boundaryInCTM {
|
|
object = compositeBoundary(object, node.parent, false)
|
|
}
|
|
object.PathObject.Clips = node.object.PathObject.Clips
|
|
return node.update(object)
|
|
})
|
|
}
|
|
|
|
// collectCompositeIDs 为已校验副本中的标准标识分配新值,拒绝重复标识
|
|
// 入参: node 原节点, ids 原标识与新标识
|
|
// 返回: error 错误信息
|
|
func (e *Editor) collectCompositeIDs(node *editorXML, ids map[string]string) error {
|
|
if id := editorResourceID(node.attr("ID")); id != "" {
|
|
if ids[id] != "" {
|
|
return fmt.Errorf("duplicate object ID %q", id)
|
|
}
|
|
ids[id] = e.nextID()
|
|
}
|
|
for _, child := range node.children {
|
|
if err := e.collectCompositeIDs(child, ids); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Style 获取内部成员的独立样式快照,文字和路径CTM只表达页面描边倍率
|
|
// 可传入CopyStyle或CopyCompositeStyle,不作为几何复制快照
|
|
// 返回: GraphicObject 有效样式来源
|
|
func (m CompositeMember) Style() GraphicObject {
|
|
object := cloneEditorData(m.Object)
|
|
if object.Type == "PathObject" {
|
|
object.PathObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
|
} else if object.Type == "TextObject" {
|
|
object.TextObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
|
}
|
|
return object
|
|
}
|
|
|
|
// CopyCompositeStyle 将同类型样式复制到内部选区,保留内容、位置、裁剪及其他实例
|
|
// 描边复制页面实际线宽和虚线,文字沿用目标段落和基线,全部成功后提交一次撤销
|
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, source Object或CompositeMember.Style取得的样式快照
|
|
// 返回: error 错误信息
|
|
func (e *Editor) CopyCompositeStyle(page int, path ObjectPath, indexes []int, source GraphicObject) error {
|
|
if err := e.validateCopyStyle(source); err != nil {
|
|
return err
|
|
}
|
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
|
if len(nodes) == 0 {
|
|
return nil
|
|
}
|
|
var err error
|
|
source, err = e.explicitDefaultPaint(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, node := range nodes {
|
|
member := members[i]
|
|
if node.object.Type != source.Type || !member.Capabilities.Transform || source.Type != "ImageObject" && !member.Capabilities.Paint {
|
|
return fmt.Errorf("style requires editable members of the same type")
|
|
}
|
|
object := cloneEditorData(member.Object)
|
|
switch source.Type {
|
|
case "TextObject":
|
|
if err := e.updateCompositeText(node, member, nil, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size}, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
var err error
|
|
object, err = e.compositeMemberStyle(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
style := copyEditorPathStyle(object.TextObject.pathStyle(), source.TextObject.pathStyle(), editorStrokeScale(source.TextObject.CTM)/member.StrokeScale)
|
|
if err := validateEditorStroke(style); err != nil {
|
|
return err
|
|
}
|
|
object.TextObject = object.TextObject.withPathStyle(style)
|
|
object.TextObject.DrawParam, err = e.neutralDrawParam()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "PathObject":
|
|
object.PathObject = copyEditorPathStyle(object.PathObject, source.PathObject, editorStrokeScale(source.PathObject.CTM)/member.StrokeScale)
|
|
if err := validateEditorStroke(object.PathObject); err != nil {
|
|
return err
|
|
}
|
|
var err error
|
|
object.PathObject.DrawParam, err = e.neutralDrawParam()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "ImageObject":
|
|
object.ImageObject.Alpha = cloneEditorData(source.ImageObject.Alpha)
|
|
}
|
|
if err := node.update(object); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|