feat(剪裁编辑): 规范裁剪编辑,优化资源扫描
build.yaml / build (push) Successful in 43s

This commit is contained in:
xiaoqidun committed 2026-10-09 19:02:02 +08:00
1 parent 2c96b4e490
commit 32ab7fe732
18 files changed
+83 -56

No files matched your search

+1 -1
View File
@@ -1035,7 +1035,7 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
default:
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
}
if clips := editorObjectClips(&object); scale != 1 && *clips != nil && (object.Type == "TextObject" || (*clips).TransFlag != nil && !*(*clips).TransFlag) {
if clips := editorObjectClips(&object); scale != 1 && *clips != nil && (object.Type == "TextObject" || !(*clips).usesObjectMatrix()) {
*clips = transformObjectClips(*clips, Matrix{a: scale, d: scale})
}
box, err := ParseBox(*boundary)
+5 -7
View File
@@ -470,10 +470,8 @@ func (e *Editor) compositeMembers(n *editorCompositeNode, reader *Reader, render
boundary := n.parent.Multiply(TranslationMatrix(box.X, box.Y))
matrix := boundary.Multiply(NewMatrix(c.CTM))
clips, clipMatrix := c.Clips, matrix
if clips != nil && clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips
copy.TransFlag = nil
clips, clipMatrix = &copy, boundary
if !clips.usesObjectMatrix() {
clipMatrix = boundary
}
clip := n.clip
var err error
@@ -658,7 +656,7 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
}
if transform && node.object.Type == "ImageObject" && bounds.Bounds.W > 0 && bounds.Bounds.H > 0 {
clips := node.object.ImageObject.Clips
_, capability.CropImage = node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
_, capability.CropImage = node.matrix(clips.usesObjectMatrix()).Invert()
capability.FitImage = capability.CropImage && axisAlignedMatrix(NewMatrix(node.object.ImageObject.CTM))
capability.ResetCrop = node.states[editorObjectID(node.object)].crop != nil
}
@@ -903,7 +901,7 @@ func transformCompositeOffsets(data []byte, matrix Matrix) ([]byte, error) {
}
object := cloneEditorData(n.object)
object = compositeBoundary(object, Matrix{a: matrix.a, b: matrix.b, c: matrix.c, d: matrix.d}, false)
if clips := editorObjectClips(&object); *clips != nil && (*clips).TransFlag != nil && !*(*clips).TransFlag {
if clips := editorObjectClips(&object); *clips != nil && !(*clips).usesObjectMatrix() {
*clips = transformObjectClips(*clips, TranslationMatrix(matrix.e, matrix.f))
}
fragment, err = editorXMLObject(n.data, n.node, n.object, object)
@@ -943,7 +941,7 @@ func compositeBoundary(object GraphicObject, parent Matrix, inward bool) Graphic
case "ImageObject":
object.ImageObject.Boundary = editorBoxString(box)
}
if clips := editorObjectClips(&object); *clips != nil && (*clips).TransFlag != nil && !*(*clips).TransFlag {
if clips := editorObjectClips(&object); *clips != nil && !(*clips).usesObjectMatrix() {
*clips = transformObjectClips(*clips, clipMatrix)
}
return object
+1 -1
View File
@@ -299,7 +299,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
return err
}
clips := *editorObjectClips(&copy.object)
matrix, ok := copy.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
matrix, ok := copy.matrix(clips.usesObjectMatrix()).Invert()
if !ok {
return fmt.Errorf("composite clip transform is not invertible")
}
+9 -5
View File
@@ -43,7 +43,7 @@ func (e *Editor) CropCompositeImage(page int, path ObjectPath, index int, box Bo
return fmt.Errorf("crop does not intersect the image")
}
clips := node.object.ImageObject.Clips
inverse, _ := node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
inverse, _ := node.matrix(clips.usesObjectMatrix()).Invert()
shape, err := NewShape(ShapeRectangle, box)
if err != nil {
return err
@@ -143,7 +143,7 @@ func (e *Editor) FitCompositeImage(page int, path ObjectPath, index int, mode st
frame = TranslationMatrix(box.X, box.Y).Multiply(node.parent)
}
clips := object.ImageObject.Clips
inverse, _ := node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
inverse, _ := node.matrix(clips.usesObjectMatrix()).Invert()
return appendCompositeClip(node, shape, inverse.Multiply(frame), true)
})
}
@@ -192,7 +192,7 @@ func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Mat
state.crop = &crop
}
data, err := encodeOFDXML(func(x *ofdXML) {
x.clips(&Clips{Clip: []Clip{clip}})
x.clips(&Clips{Clip: []Clip{clip}}, "")
})
if err != nil {
return err
@@ -209,8 +209,12 @@ func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Mat
patch = editorXMLContent(node.data, original, content)
} else {
data = editorXMLEncodedFragment(data, root)
content := append(data, node.data[node.node.open:node.node.close]...)
patch = editorXMLContent(node.data, node.node, content)
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 {
+2 -2
View File
@@ -133,7 +133,7 @@ func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int,
return fmt.Errorf("composite member cannot be erased")
}
clips := *editorObjectClips(&node.object)
inverse, ok := node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
inverse, ok := node.matrix(clips.usesObjectMatrix()).Invert()
if !ok {
return fmt.Errorf("composite clip transform is not invertible")
}
@@ -212,7 +212,7 @@ func (e *Editor) eraseObjects(page int, ids []string, box Box, points []Point, r
*clips = &Clips{}
}
matrix := IdentityMatrix
if (*clips).TransFlag == nil || *(*clips).TransFlag {
if (*clips).usesObjectMatrix() {
if object.Type == "ImageObject" && ctm == "" {
ctm = Matrix{a: frame.W, d: frame.H}.String()
}
+5
View File
@@ -261,6 +261,11 @@ func (x *ofdXML) referenceElement(name string, value any) {
}
var attrs ofdAttrs
switch value := value.(type) {
case *Dest:
if value == nil {
return
}
attrs.add("PageID", value.PageID)
case ShdColor:
attrs.add("ColorSpace", value.ColorSpace)
case *ShdColor:
+4 -9
View File
@@ -456,7 +456,7 @@ func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject {
local := TranslationMatrix(-after.X, -after.Y).Multiply(matrix).Multiply(TranslationMatrix(before.X, before.Y))
*ctm = local.Multiply(NewMatrix(*ctm)).String()
*boundary = editorBoxString(after)
if clips := editorObjectClips(&object); *clips != nil && (*clips).TransFlag != nil && !*(*clips).TransFlag {
if clips := editorObjectClips(&object); *clips != nil && !(*clips).usesObjectMatrix() {
*clips = transformObjectClips(*clips, local)
}
if !axisAlignedMatrix(matrix) && before.W > 0 && before.H > 0 {
@@ -469,13 +469,12 @@ func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject {
*clips = &copy
}
clipMatrix := local
if (*clips).TransFlag == nil || *(*clips).TransFlag {
if (*clips).usesObjectMatrix() {
if inverse, ok := NewMatrix(*ctm).Invert(); ok {
clipMatrix = inverse.Multiply(local)
} else {
*clips = transformObjectClips(*clips, NewMatrix(*ctm))
flag := false
(*clips).TransFlag = &flag
(*clips).TransFlag = nil
}
}
fill, stroke := true, false
@@ -526,17 +525,13 @@ func cropImageObject(image *ImageObject, box Box) error {
image.Boundary = editorBoxString(box)
image.Clips = nil
if box != full {
inverse, ok := NewMatrix(image.CTM).Invert()
if !ok {
return fmt.Errorf("image transform is not invertible")
}
path, err := NewShape(ShapeRectangle, Box{W: box.W, H: box.H})
if err != nil {
return err
}
fill, stroke := true, false
path.Fill, path.Stroke = &fill, &stroke
image.Clips = &Clips{Clip: []Clip{{Area: []ClipArea{{CTM: inverse.String(), Path: []PathObject{path}}}}}}
image.Clips = &Clips{Clip: []Clip{{Area: []ClipArea{{Path: []PathObject{path}}}}}}
}
return nil
}
+16
View File
@@ -373,6 +373,19 @@ func editorXMLObject(data []byte, node *editorXML, before, after GraphicObject)
// 入参: data 原文, node 原节点, oldXML 修改前编码, newXML 修改后编码
// 返回: []byte 更新节点, error 错误信息
func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte, error) {
legacyClip := node.name.Local == "Clips" && node.hasAttr("TransFlag")
if legacyClip {
var clips Clips
if err := xml.Unmarshal(data[node.start:node.end], &clips); err != nil {
return nil, err
}
clips.TransFlag = nil
var err error
oldXML, err = encodeOFDXML(func(x *ofdXML) { x.clips(&clips, "") })
if err != nil {
return nil, err
}
}
oldNode, err := parseEditorXML(oldXML)
if err != nil {
return nil, err
@@ -382,6 +395,9 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
return nil, err
}
changes := make(map[string]string)
if legacyClip {
changes["TransFlag"] = ""
}
for _, attr := range append(oldNode.attrs, newNode.attrs...) {
if attr.Name.Space == "" && (oldNode.attr(attr.Name.Local) != newNode.attr(attr.Name.Local) || oldNode.hasAttr(attr.Name.Local) != newNode.hasAttr(attr.Name.Local)) {
changes[attr.Name.Local] = newNode.attr(attr.Name.Local)
+1 -1
View File
@@ -47,7 +47,7 @@ type StrokeOptions struct {
}
// GeometryBackend 操作库自有路径,坐标以页面左上角为原点,不修改任何输入
// Clip返回nil表示不裁剪,空路径指针表示完全裁去,矩阵映射到页面坐标
// Clip返回nil表示不裁剪,空路径指针表示完全裁去,矩阵将裁剪坐标映射到页面坐标
type GeometryBackend interface {
Backend
Path(object PathObject) (GeometryPath, error)
+6
View File
@@ -47,6 +47,12 @@ func (o GraphicObject) Actions() []Action {
return nil
}
// usesObjectMatrix 判断旧式扩展是否要求裁剪跟随对象变换,标准裁剪以外接矩形为基准
// 返回: bool 是否叠加对象CTM
func (clips *Clips) usesObjectMatrix() bool {
return clips != nil && clips.TransFlag != nil && *clips.TransFlag
}
// UnmarshalXML 解析路径并区分省略样式、显式实线与零线宽
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
+2 -1
View File
@@ -62,7 +62,8 @@ type GraphicObject struct {
state editorCompositeState
}
// Clips 裁剪区域集合
// Clips 相对于对象外接矩形的裁剪集合
// TransFlag仅兼容旧式扩展,写入时折算为Area的CTM
type Clips struct {
TransFlag *bool `xml:"TransFlag,attr"`
Clip []Clip `xml:"Clip"`
+1 -1
View File
@@ -502,7 +502,7 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
continue
}
if clips == nil {
clips = &Clips{TransFlag: new(false), Clip: make([]Clip, 0, len(paths))}
clips = &Clips{Clip: make([]Clip, 0, len(paths))}
areas = make([]ClipArea, len(paths))
if origin.X != 0 || origin.Y != 0 {
translation = pdfNumbers(1, 0, 0, 1, -origin.X, -origin.Y)
+3 -3
View File
@@ -74,7 +74,7 @@ func pdfPageClip(object GraphicObject) *Clips {
return nil
}
clips := *editorObjectClips(&object)
if clips == nil || clips.TransFlag == nil || *clips.TransFlag || len(clips.Clip) == 0 {
if clips == nil || clips.usesObjectMatrix() || len(clips.Clip) == 0 {
return nil
}
boundary, _ := editorGeometry(object)
@@ -87,7 +87,7 @@ func pdfPageClip(object GraphicObject) *Clips {
translation = pdfNumbers(1, 0, 0, 1, -box.X, -box.Y)
}
bytes := 0
result := &Clips{TransFlag: clips.TransFlag, Clip: make([]Clip, len(clips.Clip))}
result := &Clips{Clip: make([]Clip, len(clips.Clip))}
for index, clip := range clips.Clip {
if len(clip.Area) != 1 || clip.Area[0].CTM != translation {
return nil
@@ -113,7 +113,7 @@ func pdfMatchesPageClip(object GraphicObject, pageClip *Clips) bool {
return false
}
clips := *editorObjectClips(&object)
if clips == nil || clips.TransFlag == nil || *clips.TransFlag || len(clips.Clip) != len(pageClip.Clip) {
if clips == nil || clips.usesObjectMatrix() || len(clips.Clip) != len(pageClip.Clip) {
return false
}
boundary, _ := editorGeometry(object)
+1 -4
View File
@@ -373,10 +373,7 @@ func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matri
return clip, nil
}
matrix, _ := renderObjectMatrix(boundary, local, state)
if clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips
copy.TransFlag = nil
clips = &copy
if !clips.usesObjectMatrix() {
box, _ := ParseBox(boundary)
matrix = TranslationMatrix(box.X, box.Y)
if state.BoundaryInCTM && state.Parent != nil {
+2 -4
View File
@@ -225,10 +225,8 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
return err
}
clips, clipMatrix := object.Clips, matrix
if clips != nil && clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips
copy.TransFlag = nil
clips, clipMatrix = &copy, boundary
if !clips.usesObjectMatrix() {
clipMatrix = boundary
}
if clips != nil {
geometry, err := r.Geometry()
+3 -6
View File
@@ -86,9 +86,9 @@ func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, boundary str
}
// buildClipPath 构建裁剪路径
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, matrix 裁剪坐标到页面的变换
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix) *canvas.Path {
func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, matrix Matrix) *canvas.Path {
if clips == nil {
return nil
}
@@ -96,10 +96,7 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ob
for _, clip := range clips.Clip {
renderer := &clipRenderer{}
for _, area := range clip.Area {
areaCTM := NewMatrix(area.CTM)
if clips.TransFlag == nil || *clips.TransFlag {
areaCTM = objectCTM.Multiply(areaCTM)
}
areaCTM := matrix.Multiply(NewMatrix(area.CTM))
for _, pathObj := range area.Path {
renderer.add(r.buildClipAreaPath(pathObj, area.DrawParam, areaCTM, pageH, bx, by))
if r.renderError != nil {
+1 -1
View File
@@ -181,7 +181,7 @@ func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result Ge
}
// Clip 解析对象裁剪并与父裁剪相交,保留未裁剪与完全裁去的区别
// 入参: r 渲染器, clips OFD裁剪, matrix 页面变换, parent 父裁剪
// 入参: r 渲染器, clips OFD裁剪, matrix 裁剪坐标到页面的变换, parent 父裁剪
// 返回: *GeometryPath 页面裁剪, error 无效父路径错误
func (CanvasBackend) Clip(r *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) {
p, err := geometryToCanvasPath(parent)
+20 -10
View File
@@ -870,17 +870,22 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
x.start(object.Type, attrs)
x.actions(actions)
if object.Type == "ImageObject" {
x.clips(object.ImageObject.Clips)
ctm := object.ImageObject.CTM
if ctm == "" && object.ImageObject.Clips.usesObjectMatrix() {
box, _ := ParseBox(object.ImageObject.Boundary)
ctm = (Matrix{a: box.W, d: box.H}).String()
}
x.clips(object.ImageObject.Clips, ctm)
x.border(object.ImageObject.Border)
} else if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
x.clips(object.CompositeGraphicUnit.Clips)
x.clips(object.CompositeGraphicUnit.Clips, object.CompositeGraphicUnit.CTM)
} else if object.Type == "PathObject" || object.Type == "Path" {
x.clips(object.PathObject.Clips)
x.clips(object.PathObject.Clips, object.PathObject.CTM)
x.color("StrokeColor", stroke)
x.color("FillColor", fill)
x.text("AbbreviatedData", object.PathObject.AbbreviatedData)
} else if object.Type == "TextObject" || object.Type == "Text" {
x.clips(object.TextObject.Clips)
x.clips(object.TextObject.Clips, object.TextObject.CTM)
x.color("FillColor", fill)
x.color("StrokeColor", stroke)
offset, index := 0, 0
@@ -917,15 +922,17 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
x.end(object.Type)
}
// clips 写出对象裁剪,保留区域内的路径与文字
// 入参: clips 裁剪集合,nil不输出节点
func (x *ofdXML) clips(clips *Clips) {
// clips 写出标准裁剪,将旧式随动裁剪折算为区域变换
// 入参: clips 裁剪集合,nil不输出节点, ctm 对象变换矩阵
func (x *ofdXML) clips(clips *Clips, ctm string) {
if clips == nil || !x.checkContext() {
return
}
var attrs ofdAttrs
attrs.flag("TransFlag", clips.TransFlag)
x.start("Clips", attrs)
matrix := IdentityMatrix
if clips.usesObjectMatrix() {
matrix = NewMatrix(ctm)
}
x.start("Clips", nil)
for _, clip := range clips.Clip {
if x.err != nil {
return
@@ -936,6 +943,9 @@ func (x *ofdXML) clips(clips *Clips) {
return
}
var attrs ofdAttrs
if clips.usesObjectMatrix() {
area.CTM = matrix.Multiply(NewMatrix(area.CTM)).String()
}
attrs.add("CTM", area.CTM)
attrs.add("DrawParam", area.DrawParam)
for _, path := range area.Path {