Files
ofdgo/ofdgo_editor_composite_image.go
2026-10-12 10:47:20 +08:00

285 lines
9.9 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"
"reflect"
"slices"
)
// CropCompositeImage 将内部图片与页面矩形取交集,保留原裁剪、蒙版、变换和边框
// 只收窄可见区域,撤销恢复上次裁剪,不重采样图片
// 入参: page 页面索引, path 父复合路径, index 图片序号, box 页面毫米范围
// 返回: error 错误信息
func (e *Editor) CropCompositeImage(page int, path ObjectPath, index int, box Box) error {
if err := validateCreationBox(box); err != nil {
return err
}
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
node, member := nodes[0], members[0]
if !member.Capabilities.CropImage {
return fmt.Errorf("composite image cannot be cropped")
}
before := member.Bounds
if box.X <= before.X && box.Y <= before.Y && box.X+box.W >= before.X+before.W && box.Y+box.H >= before.Y+before.H {
return nil
}
if box.X+box.W <= before.X || box.Y+box.H <= before.Y || box.X >= before.X+before.W || box.Y >= before.Y+before.H {
return fmt.Errorf("crop does not intersect the image")
}
clips := node.object.ImageObject.Clips
inverse, _ := node.matrix(clips.usesObjectMatrix()).Invert()
shape, err := NewShape(ShapeRectangle, box)
if err != nil {
return err
}
fill, stroke := true, false
shape.Fill, shape.Stroke = &fill, &stroke
return appendCompositeClip(node, shape, inverse, true)
})
}
// ResetCompositeImageCrop 仅移除本次编辑追加的裁剪,保留输入文件中的裁剪、蒙版和其他属性
// 入参: page 页面索引, path 父路径, index 图片序号
// 返回: error 错误信息
func (e *Editor) ResetCompositeImageCrop(page int, path ObjectPath, index int) error {
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error {
return resetCompositeImageCrop(nodes[0])
})
}
// ResetImageCrop 还原从内部范围粘贴到页面的图片裁剪,保留原始裁剪与父范围限制
// 仅处理当前会话记录的新增裁剪,无还原记录时不修改对象
// 入参: page 页面索引, id 图片标识
// 返回: error 错误信息
func (e *Editor) ResetImageCrop(page int, id string) error {
object, err := e.Object(page, id)
if err != nil {
return err
}
if object.Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", id)
}
if object.state.crop == nil {
return nil
}
origin := e.snapshotOrigin(object)
var data []byte
if origin == nil {
data, err = editorObjectXML(object)
} else {
data, err = editorXMLObject(origin.data, origin.node, origin.object, object)
if err == nil {
data, err = editorXMLStandalone(data, origin.node)
}
}
if err != nil {
return err
}
node, err := newEditorCompositeNode(data)
if err != nil {
return err
}
node.setState(object.state)
if err := resetCompositeImageCrop(node); err != nil {
return err
}
node.object.state = node.states[id]
if origin == nil {
return e.UpdateObject(page, id, node.object)
}
node.node.parent = origin.node.parent
next := &editorObjectOrigin{page: origin.page, data: node.data, node: node.node, object: node.object}
return e.updateObjectOrigins(page, []GraphicObject{node.object}, true, map[string]*editorObjectOrigin{id: next})
}
// FitCompositeImage 按原像素比例适应或填充内部图片框,保留轴方向、原始裁剪和蒙版
// 入参: page 页面索引, path 父路径, index 图片序号, mode 为contain或cover
// 返回: error 错误信息
func (e *Editor) FitCompositeImage(page int, path ObjectPath, index int, mode string) error {
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
node := nodes[0]
if !members[0].Capabilities.FitImage {
return fmt.Errorf("composite image fitting is not supported")
}
before, changed := node.data, node.changed
id := editorObjectID(node.object)
state := node.states[id]
defer func() {
if bytes.Equal(before, node.data) && reflect.DeepEqual(state, node.states[id]) {
node.changed = changed
}
}()
if err := resetCompositeImageCrop(node); err != nil {
return err
}
object := cloneEditorData(node.object)
if err := e.fitImage(&object.ImageObject, mode); err != nil {
return err
}
object.ImageObject.Clips = node.object.ImageObject.Clips
if err := node.update(object); err != nil {
return err
}
if mode == "contain" {
return nil
}
box, _ := ParseBox(object.ImageObject.Boundary)
shape, err := NewShape(ShapeRectangle, Box{W: box.W, H: box.H})
if err != nil {
return err
}
fill, stroke := true, false
shape.Fill, shape.Stroke = &fill, &stroke
frame := node.parent.Multiply(TranslationMatrix(box.X, box.Y))
if !node.boundaryInCTM {
frame = TranslationMatrix(box.X, box.Y).Multiply(node.parent)
}
clips := object.ImageObject.Clips
inverse, ok := node.matrix(clips.usesObjectMatrix()).Invert()
if !ok {
return fmt.Errorf("fitted image transform is not invertible")
}
return appendCompositeClip(node, shape, inverse.Multiply(frame), true)
})
}
// matrix 获取对象局部坐标到页面的变换,兼容旧式内联边界
// 入参: transform 是否包含对象CTM
// 返回: Matrix 坐标变换
func (n *editorCompositeNode) matrix(transform bool) Matrix {
boundary, ctm := editorGeometry(n.object)
box, _ := ParseBox(boundary)
m := IdentityMatrix
if transform {
m = NewMatrix(ctm)
if n.object.Type == "ImageObject" && ctm == "" {
m = Matrix{a: box.W, d: box.H}
}
}
if n.boundaryInCTM || n.object.Type == "CompositeObject" || n.object.Type == "CompositeGraphicUnit" {
return n.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(m)
}
if !transform {
return TranslationMatrix(box.X, box.Y)
}
return TranslationMatrix(box.X, box.Y).Multiply(n.parent).Multiply(m)
}
// appendCompositeClip 追加独立交集裁剪并记录可还原序号,保留原始裁剪XML
// 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原序号
// 返回: error 错误信息
func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Matrix, track bool) error {
for _, value := range matrix.Values() {
if !finite(value) {
return fmt.Errorf("clip transform exceeds finite range")
}
}
clip := Clip{Area: []ClipArea{{CTM: matrix.String(), Path: []PathObject{shape}}}}
if clips := *editorObjectClips(&node.object); clips != nil && slices.ContainsFunc(clips.Clip, func(previous Clip) bool { return reflect.DeepEqual(previous, clip) }) {
return nil
}
state := node.states[editorObjectID(node.object)]
if track {
crop := editorCompositeCrop{exists: node.node.child("Clips") != nil}
if state.crop != nil {
crop = *state.crop
}
count := 0
if clips := node.object.ImageObject.Clips; clips != nil {
count = len(clips.Clip)
}
crop.indexes = append(slices.Clone(crop.indexes), count)
state.crop = &crop
}
data, err := encodeOFDXML(func(x *ofdXML) {
x.clips(&Clips{Clip: []Clip{clip}}, "")
})
if err != nil {
return err
}
data = bytes.TrimPrefix(data, []byte(xml.Header))
root, err := parseEditorXML(data)
if err != nil {
return err
}
var patch editorXMLPatch
if original := node.node.child("Clips"); original != nil {
clip, err := editorXMLEncodedFragment(data, root.children[0], original.name.Space)
if err != nil {
return err
}
content := append(bytes.Clone(node.data[original.open:original.close]), clip...)
patch = editorXMLContent(node.data, original, content)
} else {
data, err = editorXMLEncodedFragment(data, root, node.node.name.Space)
if err != nil {
return err
}
if actions := node.node.child("Actions"); actions != nil {
patch = editorXMLPatch{actions.end, actions.end, data}
} else {
content := append(data, node.data[node.node.open:node.node.close]...)
patch = editorXMLContent(node.data, node.node, content)
}
}
updated, err := newEditorCompositeNode(editorPatchXML(node.data, []editorXMLPatch{patch}))
if err != nil {
return err
}
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
node.setState(state)
return nil
}
// resetCompositeImageCrop 恢复图片原始裁剪节点,不还原后续变换和资源替换
// 入参: node 图片节点
// 返回: error 错误信息
func resetCompositeImageCrop(node *editorCompositeNode) error {
if node.object.Type != "ImageObject" {
return fmt.Errorf("composite member is not an image")
}
state := node.states[editorObjectID(node.object)]
if state.crop == nil {
return nil
}
clips := node.node.child("Clips")
var patches []editorXMLPatch
if !state.crop.exists && len(node.object.ImageObject.Clips.Clip) == len(state.crop.indexes) {
patches = append(patches, editorXMLPatch{clips.start, clips.end, nil})
} else {
count := 0
for _, child := range clips.children {
if child.name.Local == "Clip" {
if slices.Contains(state.crop.indexes, count) {
patches = append(patches, editorXMLPatch{child.start, child.end, nil})
}
count++
}
}
}
updated, err := newEditorCompositeNode(editorPatchXML(node.data, patches))
if err != nil {
return err
}
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
state.crop = nil
node.setState(state)
return nil
}