diff --git a/go.mod b/go.mod index 3f76e8f..a6a2775 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20260930033808-2918854e21c8 + github.com/xiaoqidun/pdfgo v0.0.0-20260930050241-ecc0b0d74180 golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 6772ebe..1f73e12 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20260930033808-2918854e21c8 h1:19c9Wr9PPJ/GEh4AlrDqiUgucq1bYSIvughgsPKvNis= -github.com/xiaoqidun/pdfgo v0.0.0-20260930033808-2918854e21c8/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260930050241-ecc0b0d74180 h1:2/QHfh0NIg74xzT3KHjqN7u1S9U2F+4N0HZqPrJFxvw= +github.com/xiaoqidun/pdfgo v0.0.0-20260930050241-ecc0b0d74180/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index ea5d49a..4cd23fa 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -1599,17 +1599,14 @@ func splitEditorSelection(ids []string) ([]string, []string) { // 返回: any 文档与新选区, error 错误信息 func changeSelection(args []js.Value) (any, error) { var selected []string - result, err := changeObjects(func() error { + result, err := changeAtomicObjects(func() error { page, operation := args[0].Int(), args[2].String() objects, annotations := splitEditorSelection(stringsFromJS(args[1])) switch operation { case "align", "distribute": return arrangeEditorSelection(page, stringsFromJS(args[1]), operation, args[3].String()) case "delete": - if err := currentEditor.DeleteObjects(page, objects); err != nil { - return err - } - return currentEditor.DeleteAnnotations(page, annotations) + return currentEditor.DeleteSelection(page, objects, annotations) case "copy": clipboard, err := captureEditorSelection(page, stringsFromJS(args[1])) if err != nil { @@ -1618,102 +1615,31 @@ func changeSelection(args []js.Value) (any, error) { selected, err = pasteEditorSelection(page, clipboard, args[3].Float(), args[4].Float()) return err case "style": - style := objectStyle(args[3]) - if err := currentEditor.StyleObjects(page, objects, style); err != nil { - return err - } - for _, id := range annotations { - path := ofdgo.ObjectPath{Annotation: id} - members, err := currentEditor.CompositeObjects(page, path) - if err != nil { - return err - } - indexes := make([]int, len(members)) - for i := range indexes { - indexes[i] = i - } - if err := currentEditor.StyleCompositeObjects(page, path, indexes, style); err != nil { - return err - } - } - return nil - case "erase", "erasePath": - var box ofdgo.Box - var points []ofdgo.Point - if operation == "erase" { - box = ofdgo.Box{X: args[3].Float(), Y: args[4].Float(), W: args[5].Float(), H: args[6].Float()} - if err := currentEditor.EraseObjects(page, objects, box); err != nil { - return err - } - } else { - points = pointsFromJS(args[3]) - if err := currentEditor.EraseObjectsPath(page, objects, points); err != nil { - return err - } - } - for _, id := range annotations { - path := ofdgo.ObjectPath{Annotation: id} - members, err := currentEditor.CompositeObjects(page, path) - if err != nil { - return err - } - indexes := make([]int, len(members)) - for i := range indexes { - indexes[i] = i - } - if operation == "erase" { - err = currentEditor.EraseCompositeObjects(page, path, indexes, box) - } else { - err = currentEditor.EraseCompositeObjectsPath(page, path, indexes, points) - } - if err != nil { - return err - } - } - return nil - } - matrix := ofdgo.IdentityMatrix - if operation == "transform" { + return currentEditor.StyleSelection(page, objects, annotations, objectStyle(args[3])) + case "erase": + return currentEditor.EraseSelection(page, objects, annotations, ofdgo.Box{ + X: args[3].Float(), Y: args[4].Float(), W: args[5].Float(), H: args[6].Float(), + }) + case "erasePath": + return currentEditor.EraseSelectionPath(page, objects, annotations, pointsFromJS(args[3])) + case "rotate": + return currentEditor.RotateSelection(page, objects, annotations, args[3].Int()) + case "flip": + return currentEditor.FlipSelection(page, objects, annotations, args[3].String()) + case "resize": + return currentEditor.ResizeSelection(page, objects, annotations, ofdgo.Box{ + X: args[3].Float(), Y: args[4].Float(), W: args[5].Float(), H: args[6].Float(), + }) + case "transform": dx, dy, scale := args[3].Float(), args[4].Float(), args[5].Float() if scale <= 0 { return fmt.Errorf("scale must be positive") } - matrix = ofdgo.NewMatrix(fmt.Sprintf("%g 0 0 %g %g %g", scale, scale, dx, dy)) - } else { - bounds, err := editorSelectionBounds(page, objects, annotations) - if err != nil { - return err - } - x, y := bounds.X+bounds.W/2, bounds.Y+bounds.H/2 - switch operation { - case "rotate": - angle := float64(args[3].Int()) * math.Pi / 180 - matrix = ofdgo.NewMatrix(fmt.Sprintf("%g %g %g %g 0 0", math.Cos(angle), math.Sin(angle), -math.Sin(angle), math.Cos(angle))) - case "flip": - if args[3].String() == "horizontal" { - matrix = ofdgo.NewMatrix("-1 0 0 1 0 0") - } else if args[3].String() == "vertical" { - matrix = ofdgo.NewMatrix("1 0 0 -1 0 0") - } else { - return fmt.Errorf("invalid flip axis") - } - case "resize": - sx, sy := args[5].Float()/bounds.W, args[6].Float()/bounds.H - if sx <= 0 || sy <= 0 { - return fmt.Errorf("size must be positive") - } - matrix = ofdgo.NewMatrix(fmt.Sprintf("%g 0 0 %g %g %g", sx, sy, args[3].Float()-bounds.X*sx, args[4].Float()-bounds.Y*sy)) - default: - return fmt.Errorf("unsupported mixed selection operation %q", operation) - } - if operation != "resize" { - matrix = ofdgo.TranslationMatrix(x, y).Multiply(matrix).Multiply(ofdgo.TranslationMatrix(-x, -y)) - } + matrix := ofdgo.NewMatrix(fmt.Sprintf("%g 0 0 %g %g %g", scale, scale, dx, dy)) + return currentEditor.TransformSelection(page, objects, annotations, matrix) + default: + return fmt.Errorf("unsupported mixed selection operation %q", operation) } - if err := currentEditor.TransformObjectsMatrix(page, objects, matrix); err != nil { - return err - } - return currentEditor.TransformAnnotations(page, annotations, matrix) }) if err != nil || selected == nil { return result, err @@ -1736,13 +1662,6 @@ func arrangeEditorSelection(page int, ids []string, operation, direction string) } } -// editorSelectionBounds 计算混合选区的实际内容范围 -// 入参: page 页面索引, objects 正文标识, annotations 注解标识 -// 返回: ofdgo.Box 页面毫米范围, error 错误信息 -func editorSelectionBounds(page int, objects, annotations []string) (ofdgo.Box, error) { - return currentEditor.SelectionBounds(page, objects, annotations) -} - // captureEditorSelection 捕获正文与注解的独立副本 // 入参: page 页面索引, ids 画布标识 // 返回: *editorClipboard 快照, error 错误信息 @@ -3828,11 +3747,20 @@ func eraseObjects(args []js.Value) (any, error) { // 入参: apply 待执行的编辑操作 // 返回: any 文档信息, error 错误信息 func changeObjects(apply func() error) (any, error) { + return changeAtomicObjects(func() error { + return currentEditor.Transaction(func(*ofdgo.Editor) error { return apply() }) + }) +} + +// changeAtomicObjects 执行库层原子操作,按文档版本更新预览 +// 入参: apply 已具备事务保证的编辑操作 +// 返回: any 文档信息, error 错误信息 +func changeAtomicObjects(apply func() error) (any, error) { if currentEditor == nil { return nil, fmt.Errorf("no document is being edited") } revision := currentEditor.Revision() - if err := currentEditor.Transaction(func(*ofdgo.Editor) error { return apply() }); err != nil { + if err := apply(); err != nil { return nil, err } if currentEditor.Revision() == revision { diff --git a/ofdgo_editor_composite.go b/ofdgo_editor_composite.go index d1a0d4f..81c98d6 100644 --- a/ofdgo_editor_composite.go +++ b/ofdgo_editor_composite.go @@ -162,10 +162,10 @@ func (e *Editor) TransformCompositeObjects(page int, path ObjectPath, indexes [] // 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, degrees 顺时针角度,为90度的整数倍 // 返回: error 错误信息 func (e *Editor) RotateCompositeObjects(page int, path ObjectPath, indexes []int, degrees int) error { - if degrees%90 != 0 { - return fmt.Errorf("rotation must be a multiple of 90 degrees") + m, err := editorRotation(degrees) + if err != nil { + return err } - m := []Matrix{IdentityMatrix, {b: 1, c: -1}, {a: -1, d: -1}, {b: -1, c: 1}}[(degrees%360+360)%360/90] return e.changeCompositeObjects(page, path, indexes, func(box Box) Matrix { return compositeOrientation(box, m) }) } @@ -173,14 +173,9 @@ func (e *Editor) RotateCompositeObjects(page int, path ObjectPath, indexes []int // 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, axis 为horizontal或vertical // 返回: error 错误信息 func (e *Editor) FlipCompositeObjects(page int, path ObjectPath, indexes []int, axis string) error { - m := IdentityMatrix - switch axis { - case "horizontal": - m.a = -1 - case "vertical": - m.d = -1 - default: - return fmt.Errorf("invalid flip axis %q", axis) + m, err := editorFlip(axis) + if err != nil { + return err } return e.changeCompositeObjects(page, path, indexes, func(box Box) Matrix { return compositeOrientation(box, m) }) } diff --git a/ofdgo_editor_selection.go b/ofdgo_editor_selection.go index 689d58b..dbd9567 100644 --- a/ofdgo_editor_selection.go +++ b/ofdgo_editor_selection.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + "math" "slices" ) @@ -37,6 +38,88 @@ func (e *Editor) SelectionBounds(page int, objects, annotations []string) (Box, return bounds, nil } +// TransformSelection 原子变换正文与注解,失败回滚,一次撤销恢复全部 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, matrix 有限可逆变换 +// 返回: error 标识、变换或编辑错误 +func (e *Editor) TransformSelection(page int, objects, annotations []string, matrix Matrix) error { + if _, ok := matrix.Invert(); !ok || !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) || !finite(matrix.e) || !finite(matrix.f) { + return fmt.Errorf("selection transform must be finite and invertible") + } + if matrix == IdentityMatrix { + if _, _, err := e.selectedObjects(page, objects); err != nil { + return err + } + seen := make(map[string]bool, len(annotations)) + for _, id := range annotations { + if seen[id] { + return fmt.Errorf("duplicate annotation ID %q", id) + } + seen[id] = true + if _, err := e.Annotation(page, id); err != nil { + return err + } + } + return nil + } + return e.Transaction(func(edit *Editor) error { + if err := edit.TransformObjectsMatrix(page, objects, matrix); err != nil { + return err + } + return edit.TransformAnnotations(page, annotations, matrix) + }) +} + +// RotateSelection 绕正文与注解的共同可见范围中心旋转,保留相对位置及资源 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, degrees 顺时针角度,仅支持90度的整数倍 +// 返回: error 标识、度量或编辑错误 +func (e *Editor) RotateSelection(page int, objects, annotations []string, degrees int) error { + matrix, err := editorRotation(degrees) + if err != nil { + return err + } + return e.orientSelection(page, objects, annotations, matrix) +} + +// FlipSelection 绕正文与注解的共同可见范围中心镜像,保留绘制层级 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, axis 为horizontal或vertical +// 返回: error 标识、度量或编辑错误 +func (e *Editor) FlipSelection(page int, objects, annotations []string, axis string) error { + matrix, err := editorFlip(axis) + if err != nil { + return err + } + return e.orientSelection(page, objects, annotations, matrix) +} + +// ResizeSelection 按共同可见范围调整正文与注解的位置及尺寸,不重排文字或重采样图片 +// 正文独立宽高遵循ResizeObjects规则,一次撤销恢复全部 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, box 页面毫米目标范围 +// 返回: error 标识、度量或编辑错误 +func (e *Editor) ResizeSelection(page int, objects, annotations []string, box Box) error { + if _, err := creationBox(editorBoxString(box)); err != nil { + return err + } + bounds, err := e.SelectionBounds(page, objects, annotations) + if err != nil { + return err + } + sx, sy := box.W/bounds.W, box.H/bounds.H + if math.Abs(sx-sy) > 1e-9*math.Max(sx, sy) { + items, _, err := e.selectedObjects(page, objects) + if err != nil { + return err + } + for _, object := range items { + if !e.objectStretchable(object, make(map[string]bool)) { + return fmt.Errorf("nonuniform resizing requires images or image-only groups") + } + } + } else { + sy = sx + } + return e.TransformSelection(page, objects, annotations, Matrix{a: sx, d: sy, e: box.X - bounds.X*sx, f: box.Y - bounds.Y*sy}) +} + // AlignSelection 将正文与注解统一对齐,单选对齐页面,多选对齐选区,一次撤销恢复全部 // 入参: page 页面索引, objects 正文标识, annotations 注解标识, alignment 为left、center、right、top、middle或bottom // 返回: error 标识、度量或编辑错误 @@ -79,6 +162,104 @@ func (e *Editor) DistributeSelection(page int, objects, annotations []string, ax return e.transformSelection(page, objects, annotations, matrices) } +// DeleteSelection 原子删除正文与注解,一次撤销恢复全部 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识 +// 返回: error 标识或编辑错误 +func (e *Editor) DeleteSelection(page int, objects, annotations []string) error { + return e.Transaction(func(edit *Editor) error { + if err := edit.DeleteObjects(page, objects); err != nil { + return err + } + return edit.DeleteAnnotations(page, annotations) + }) +} + +// StyleSelection 原子设置正文与注解内部图元的样式,保留未指定属性 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, style 样式增量 +// 返回: error 标识、样式或编辑错误 +func (e *Editor) StyleSelection(page int, objects, annotations []string, style ObjectStyle) error { + return e.Transaction(func(edit *Editor) error { + if err := edit.StyleObjects(page, objects, style); err != nil { + return err + } + return edit.editSelectionAnnotations(page, annotations, func(path ObjectPath, indexes []int) error { + return edit.StyleCompositeObjects(page, path, indexes, style) + }) + }) +} + +// EraseSelection 按页面矩形原子擦除正文与注解内部内容,不删除其他选区成员 +// 保留原始资源,不用于敏感信息脱敏 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, box 页面毫米擦除范围 +// 返回: error 标识、范围或编辑错误 +func (e *Editor) EraseSelection(page int, objects, annotations []string, box Box) error { + return e.Transaction(func(edit *Editor) error { + if err := edit.EraseObjects(page, objects, box); err != nil { + return err + } + return edit.editSelectionAnnotations(page, annotations, func(path ObjectPath, indexes []int) error { + return edit.EraseCompositeObjects(page, path, indexes, box) + }) + }) +} + +// EraseSelectionPath 按页面多边形原子擦除正文与注解内部内容,不改变未擦除内容 +// 保留原始资源,不用于敏感信息脱敏 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, points 页面毫米多边形 +// 返回: error 标识、多边形或编辑错误 +func (e *Editor) EraseSelectionPath(page int, objects, annotations []string, points []Point) error { + return e.Transaction(func(edit *Editor) error { + if err := edit.EraseObjectsPath(page, objects, points); err != nil { + return err + } + return edit.editSelectionAnnotations(page, annotations, func(path ObjectPath, indexes []int) error { + return edit.EraseCompositeObjectsPath(page, path, indexes, points) + }) + }) +} + +// orientSelection 将直角旋转或镜像定位到混合选区中心,恒等变换仅校验标识 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, matrix 方向变换 +// 返回: error 标识、度量或编辑错误 +func (e *Editor) orientSelection(page int, objects, annotations []string, matrix Matrix) error { + if matrix != IdentityMatrix { + bounds, err := e.SelectionBounds(page, objects, annotations) + if err != nil { + return err + } + x, y := bounds.X+bounds.W/2, bounds.Y+bounds.H/2 + matrix = TranslationMatrix(x, y).Multiply(matrix).Multiply(TranslationMatrix(-x, -y)) + } + return e.TransformSelection(page, objects, annotations, matrix) +} + +// editSelectionAnnotations 对注解直接成员统一操作,重复或不存在的标识返回错误 +// 入参: page 页面索引, ids 注解标识, apply 成员操作 +// 返回: error 标识、度量或编辑错误 +func (e *Editor) editSelectionAnnotations(page int, ids []string, apply func(ObjectPath, []int) error) error { + seen := make(map[string]bool, len(ids)) + for _, id := range ids { + if seen[id] { + return fmt.Errorf("duplicate annotation ID %q", id) + } + seen[id] = true + path := ObjectPath{Annotation: id} + reader, _, _, members, err := e.compositeScope(page, path) + if err != nil { + return err + } + reader.Close() + indexes := make([]int, len(members)) + for i := range indexes { + indexes[i] = i + } + if err := apply(path, indexes); err != nil { + return err + } + } + return nil +} + // selectionGeometry 一次读取页面与资源,按正文、注解标识顺序度量并保留绘制位置 // 入参: page 页面索引, objects 正文标识, annotations 注解标识 // 返回: []Box 成员范围, []editorObjectPosition 绘制位置, Box 页面范围, error 标识或度量错误 diff --git a/ofdgo_editor_transform.go b/ofdgo_editor_transform.go index 5daa957..a3f60dd 100644 --- a/ofdgo_editor_transform.go +++ b/ofdgo_editor_transform.go @@ -51,12 +51,11 @@ func (e *Editor) RotateObject(page int, id string, degrees int) error { // 入参: page 页面索引, ids 对象标识, degrees 顺时针角度,仅支持90度的整数倍 // 返回: error 错误信息 func (e *Editor) RotateObjects(page int, ids []string, degrees int) error { - if degrees%90 != 0 { - return fmt.Errorf("rotation must be a multiple of 90 degrees") + matrix, err := editorRotation(degrees) + if err != nil { + return err } - turn := (degrees%360 + 360) % 360 / 90 - m := []Matrix{IdentityMatrix, {b: 1, c: -1}, {a: -1, d: -1}, {b: -1, c: 1}}[turn] - return e.orientObjects(page, ids, m) + return e.orientObjects(page, ids, matrix) } // FlipObject 绕对象可见范围中心镜像 @@ -71,16 +70,11 @@ func (e *Editor) FlipObject(page int, id, axis string) error { // 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical // 返回: error 错误信息 func (e *Editor) FlipObjects(page int, ids []string, axis string) error { - m := IdentityMatrix - switch axis { - case "horizontal": - m.a = -1 - case "vertical": - m.d = -1 - default: - return fmt.Errorf("invalid flip axis %q", axis) + matrix, err := editorFlip(axis) + if err != nil { + return err } - return e.orientObjects(page, ids, m) + return e.orientObjects(page, ids, matrix) } // ResizeObjects 按选区可见范围计算目标矩形的变换,图片及纯图片资源支持独立宽高,描边遵循OFD变换规则 @@ -232,6 +226,33 @@ func (e *Editor) transformObject(object GraphicObject, dx, dy, scale float64) (G return transformEditorObject(object, dx, dy, scale) } +// editorRotation 返回无浮点误差的直角旋转矩阵 +// 入参: degrees 顺时针角度,仅支持90度的整数倍 +// 返回: Matrix 旋转, error 角度错误 +func editorRotation(degrees int) (Matrix, error) { + if degrees%90 != 0 { + return Matrix{}, fmt.Errorf("rotation must be a multiple of 90 degrees") + } + turn := (degrees%360 + 360) % 360 / 90 + return [...]Matrix{IdentityMatrix, {b: 1, c: -1}, {a: -1, d: -1}, {b: -1, c: 1}}[turn], nil +} + +// editorFlip 返回指定方向的镜像矩阵 +// 入参: axis 为horizontal或vertical +// 返回: Matrix 镜像, error 方向错误 +func editorFlip(axis string) (Matrix, error) { + matrix := IdentityMatrix + switch axis { + case "horizontal": + matrix.a = -1 + case "vertical": + matrix.d = -1 + default: + return Matrix{}, fmt.Errorf("invalid flip axis %q", axis) + } + return matrix, nil +} + // orientObjects 更新Boundary与CTM,不重排文字或重采样图片 // 入参: page 页面索引, ids 对象标识, matrix 直角旋转或镜像矩阵 // 返回: error 错误信息