// 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 ( "cmp" "context" "fmt" "io" "math" "slices" "strings" ) // ObjectSelection 保存正文、注解及组合成员的独立快照,共享当前编辑器的不可变资源 // 来源修改、删除或撤销不改变快照,不用于跨文档传输 type ObjectSelection struct { editor *Editor document editorDocumentIdentity objects []GraphicObject annotations *AnnotationSelection parts []editorSelectionPart } // editorSelectionPart 保存按绘制顺序排列的顶层或内部对象快照 type editorSelectionPart struct { objects []GraphicObject composite *CompositeSelection } // SelectionBounds 获取正文与注解混合选区的实际范围,复用同一次度量的资源缓存 // 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复 // 返回: Box 页面毫米范围, error 标识或度量错误 func (e *Editor) SelectionBounds(page int, objects, annotations []string) (Box, error) { boxes, _, _, err := e.selectionGeometry(page, objects, annotations) if err != nil { return Box{}, err } var bounds Box for _, box := range boxes { bounds = unionTextBox(bounds, box) } if bounds.W <= 0 || bounds.H <= 0 { return Box{}, fmt.Errorf("selection has no visible bounds") } 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 { 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 := validateCreationBox(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 标识、度量或编辑错误 func (e *Editor) AlignSelection(page int, objects, annotations []string, alignment string) error { if !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, alignment) { return fmt.Errorf("invalid alignment %q", alignment) } boxes, _, target, err := e.selectionGeometry(page, objects, annotations) if err != nil || len(boxes) == 0 { return err } if len(boxes) > 1 { target = Box{} for _, box := range boxes { target = unionTextBox(target, box) } } matrices := make([]Matrix, len(boxes)) for i, box := range boxes { matrices[i] = editorAlignment(box, target, alignment) } return e.transformSelection(page, objects, annotations, matrices) } // DistributeSelection 按正文与注解实际范围等距分布,固定两端并校验位置顺序,一次撤销恢复全部 // 入参: page 页面索引, objects 正文标识, annotations 注解标识, axis 为horizontal或vertical // 返回: error 标识、度量、重叠或编辑错误 func (e *Editor) DistributeSelection(page int, objects, annotations []string, axis string) error { if axis != "horizontal" && axis != "vertical" { return fmt.Errorf("invalid distribution axis %q", axis) } boxes, positions, _, err := e.selectionGeometry(page, objects, annotations) if err != nil || len(boxes) < 3 { return err } matrices, err := editorDistribution(boxes, positions, axis) if err != nil { return err } 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 标识或度量错误 func (e *Editor) selectionGeometry(page int, objects, annotations []string) ([]Box, []editorObjectPosition, Box, error) { items, positions, err := e.selectedObjects(page, objects) if err != nil { return nil, nil, Box{}, err } reader, err := e.reader(nil) if err != nil { return nil, nil, Box{}, err } defer reader.Close() content, err := reader.PageContentByIndex(page) if err != nil { return nil, nil, Box{}, err } renderer := e.newRenderer(reader) paper, err := renderer.GetPageBox(content) if err != nil { return nil, nil, Box{}, err } boxes := make([]Box, 0, len(items)+len(annotations)) for i, item := range items { box, err := renderer.ObjectBounds(item, e.pages[page].Content.Layer[positions[i].layer].DrawParam) if err != nil { return nil, nil, Box{}, err } boxes = append(boxes, box) } list := reader.Annots[content.ID] lookup := make(map[string]int, len(list)) for i, annotation := range list { lookup[annotation.ID] = i } seen := make(map[string]bool, len(annotations)) for _, id := range annotations { index, found := lookup[id] if seen[id] || !found { return nil, nil, Box{}, fmt.Errorf("duplicate or missing annotation %q", id) } seen[id] = true box, _, err := renderer.AnnotationGeometry(list[index]) if err != nil { return nil, nil, Box{}, err } if box.W <= 0 || box.H <= 0 { box, err = ParseBox(list[index].Appearance.Boundary) if err != nil { return nil, nil, Box{}, err } } boxes = append(boxes, box) positions = append(positions, editorObjectPosition{layer: len(content.Content.Layer), index: index}) } for _, box := range boxes { if !finite(box.X) || !finite(box.Y) || !finite(box.W) || !finite(box.H) || box.W <= 0 || box.H <= 0 { return nil, nil, Box{}, fmt.Errorf("selection member has no visible bounds") } } return boxes, positions, paper, nil } // transformSelection 原子应用成员平移,跳过未改变的成员及历史记录 // 入参: page 页面索引, objects 正文标识, annotations 注解标识, matrices 按正文、注解排列的平移 // 返回: error 编辑错误 func (e *Editor) transformSelection(page int, objects, annotations []string, matrices []Matrix) error { return e.Transaction(func(edit *Editor) error { items, _, err := edit.selectedObjects(page, objects) if err != nil { return err } changes := make(map[string]Matrix, len(objects)) var updates []GraphicObject for i, id := range objects { if matrices[i] != IdentityMatrix { changes[id] = matrices[i] updates = append(updates, items[i]) } } if len(updates) != 0 { if err := edit.transformObjects(page, updates, func(object GraphicObject) (GraphicObject, error) { return edit.transformMatrix(object, changes[editorObjectID(object)]) }); err != nil { return err } } for i, id := range annotations { matrix := matrices[len(objects)+i] if matrix != IdentityMatrix { if err := edit.MoveAnnotations(page, []string{id}, matrix.e, matrix.f); err != nil { return err } } } return nil }) } // CaptureSelection 捕获正文与注解,不修改文档或历史 // 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复 // 返回: *ObjectSelection 独立快照, error 标识或资源错误 func (e *Editor) CaptureSelection(page int, objects, annotations []string) (*ObjectSelection, error) { items, err := e.Objects(page, objects) if err != nil { return nil, err } selection := &ObjectSelection{editor: e, document: e.documentIdentity(), objects: items} if len(annotations) != 0 { selection.annotations, err = e.CaptureAnnotations(page, annotations) if err != nil { return nil, err } } return selection, nil } // PasteSelection 原子粘贴正文与注解并平移,失败回滚,一次撤销恢复全部 // 入参: page 目标页面索引, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米 // 返回: []string 新正文标识, []string 新注解标识, error 标识、资源或编辑错误 func (e *Editor) PasteSelection(page int, selection *ObjectSelection, dx, dy float64) ([]string, []string, error) { if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) || !finite(dx) || !finite(dy) { return nil, nil, fmt.Errorf("invalid object selection or offset") } var objects, annotations []string err := e.Transaction(func(edit *Editor) error { var err error objects, err = edit.CopyObjects(page, selection.objects, dx, dy) if err == nil && selection.annotations != nil { annotations, err = edit.PasteAnnotations(page, selection.annotations, dx, dy) } for _, part := range selection.parts { if err != nil { return err } var ids []string if part.composite != nil { ids, err = edit.PasteCompositeSelection(page, part.composite, dx, dy) } else { ids, err = edit.CopyObjects(page, part.objects, dx, dy) } objects = append(objects, ids...) } return err }) if err != nil { return nil, nil, err } return objects, annotations, nil } // PasteSelectionToComposite 将正文快照粘贴至内部容器,保留源外观,拒绝嵌套注解 // 入参: page 目标页面索引, path 父复合路径, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米 // 返回: []int 新成员序号, error 选区、资源或编辑错误 func (e *Editor) PasteSelectionToComposite(page int, path ObjectPath, selection *ObjectSelection, dx, dy float64) ([]int, error) { if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) { return nil, fmt.Errorf("object selection belongs to another document or editor") } if selection.annotations != nil { return nil, fmt.Errorf("annotations cannot be nested in a page object") } var indexes []int err := e.Transaction(func(edit *Editor) error { var err error indexes, err = edit.CopyObjectsToComposite(page, path, selection.objects, dx, dy) for _, part := range selection.parts { if err != nil { return err } var next []int if part.composite != nil { next, err = edit.PasteCompositeObjects(page, path, part.composite, dx, dy) } else { next, err = edit.CopyObjectsToComposite(page, path, part.objects, dx, dy) } indexes = append(indexes, next...) } return err }) return indexes, err } // editorTargetGroup 保存同一父路径的直接成员,结构操作按逆序处理父路径 type editorTargetGroup struct { path ObjectPath indexes []int } // editorTargetGroups 分离顶层与内部选区,拒绝重复或相互包含的路径 // 入参: paths 完整对象路径 // 返回: []string 正文标识, []string 注解标识, []editorTargetGroup 内部成员, error 路径错误 func editorTargetGroups(paths []ObjectPath) ([]string, []string, []editorTargetGroup, error) { var objects, annotations []string var groups []editorTargetGroup for i, path := range paths { if (path.ID == "") == (path.Annotation == "") || slices.ContainsFunc(path.Children, func(index int) bool { return index < 0 }) { return nil, nil, nil, fmt.Errorf("invalid object path") } for _, other := range paths[:i] { length := min(len(path.Children), len(other.Children)) if path.ID == other.ID && path.Annotation == other.Annotation && slices.Equal(path.Children[:length], other.Children[:length]) { return nil, nil, nil, fmt.Errorf("selection paths overlap") } } if len(path.Children) == 0 { if path.Annotation != "" { annotations = append(annotations, path.Annotation) } else { objects = append(objects, path.ID) } continue } index := path.Children[len(path.Children)-1] path.Children = slices.Clone(path.Children[:len(path.Children)-1]) group := slices.IndexFunc(groups, func(group editorTargetGroup) bool { return path.ID == group.path.ID && path.Annotation == group.path.Annotation && slices.Equal(path.Children, group.path.Children) }) if group < 0 { groups = append(groups, editorTargetGroup{path: path, indexes: []int{index}}) } else { groups[group].indexes = append(groups[group].indexes, index) } } slices.SortFunc(groups, func(a, b editorTargetGroup) int { if order := cmp.Compare(a.path.ID, b.path.ID); order != 0 { return order } if order := cmp.Compare(a.path.Annotation, b.path.Annotation); order != 0 { return order } return -slices.Compare(a.path.Children, b.path.Children) }) return objects, annotations, groups, nil } // editTargets 原子处理跨容器选区,结构变化从后向前避免路径位移 // 入参: paths 完整对象路径, root 顶层处理, member 内部处理 // 返回: error 路径或编辑错误 func (e *Editor) editTargets(paths []ObjectPath, root func([]string, []string) error, member func(ObjectPath, []int) error) error { objects, annotations, groups, err := editorTargetGroups(paths) if err != nil { return err } return e.Transaction(func(*Editor) error { for _, group := range groups { if err := member(group.path, group.indexes); err != nil { return err } } if len(objects)+len(annotations) != 0 { return root(objects, annotations) } return nil }) } // CaptureTargets 捕获跨容器选区,内部快照保留源变换与裁剪,源删除不影响粘贴 // 入参: page 页面索引, paths 完整对象路径 // 返回: *ObjectSelection 独立快照, error 路径或资源错误 func (e *Editor) CaptureTargets(page int, paths []ObjectPath) (*ObjectSelection, error) { objects, annotations, groups, err := editorTargetGroups(paths) if err != nil { return nil, err } if len(groups) == 0 { return e.CaptureSelection(page, objects, annotations) } selection, err := e.CaptureSelection(page, nil, annotations) if err != nil { return nil, err } content, err := e.page(page) if err != nil { return nil, err } positions := e.objectPositions(content) paths = slices.Clone(paths) slices.SortStableFunc(paths, func(a, b ObjectPath) int { if a.ID != b.ID { if a.ID == "" { return 1 } if b.ID == "" { return -1 } return compareEditorPosition(positions[a.ID], positions[b.ID]) } if a.Annotation != b.Annotation { return cmp.Compare(a.Annotation, b.Annotation) } return slices.Compare(a.Children, b.Children) }) for _, path := range paths { if len(path.Children) == 0 { if path.Annotation != "" { continue } items, err := e.Objects(page, []string{path.ID}) if err != nil { return nil, err } selection.parts = append(selection.parts, editorSelectionPart{objects: items}) } else { index := path.Children[len(path.Children)-1] path.Children = path.Children[:len(path.Children)-1] composite, err := e.CaptureCompositeObjects(page, path, []int{index}) if err != nil { return nil, err } selection.parts = append(selection.parts, editorSelectionPart{composite: composite}) } } return selection, nil } // TransformTargets 按页面坐标原子变换跨容器选区,保留组合结构 // 入参: page 页面索引, paths 完整对象路径, matrix 有限可逆变换 // 返回: error 路径、变换或编辑错误 func (e *Editor) TransformTargets(page int, paths []ObjectPath, matrix Matrix) error { if _, ok := matrix.Invert(); !ok { return fmt.Errorf("selection transform must be finite and invertible") } return e.editTargets(paths, func(objects, annotations []string) error { return e.TransformSelection(page, objects, annotations, matrix) }, func(path ObjectPath, indexes []int) error { return e.changeCompositeObjects(page, path, indexes, func(Box) Matrix { return matrix }) }) } // DeleteTargets 原子删除跨容器选区,一次撤销恢复全部 // 入参: page 页面索引, paths 完整对象路径 // 返回: error 路径或编辑错误 func (e *Editor) DeleteTargets(page int, paths []ObjectPath) error { return e.editTargets(paths, func(objects, annotations []string) error { return e.DeleteSelection(page, objects, annotations) }, func(path ObjectPath, indexes []int) error { return e.DeleteCompositeObjects(page, path, indexes) }) } // StyleTargets 原子设置跨容器选区样式,一次撤销恢复全部 // 入参: page 页面索引, paths 完整对象路径, style 样式增量 // 返回: error 路径或编辑错误 func (e *Editor) StyleTargets(page int, paths []ObjectPath, style ObjectStyle) error { return e.editTargets(paths, func(objects, annotations []string) error { return e.StyleSelection(page, objects, annotations, style) }, func(path ObjectPath, indexes []int) error { return e.StyleCompositeObjects(page, path, indexes, style) }) } // EraseTargets 按页面矩形原子擦除跨容器选区,一次撤销恢复全部 // 入参: page 页面索引, paths 完整对象路径, box 页面毫米范围 // 返回: error 路径或编辑错误 func (e *Editor) EraseTargets(page int, paths []ObjectPath, box Box) error { return e.editTargets(paths, func(objects, annotations []string) error { return e.EraseSelection(page, objects, annotations, box) }, func(path ObjectPath, indexes []int) error { return e.EraseCompositeObjects(page, path, indexes, box) }) } // EraseTargetsPath 按页面多边形原子擦除跨容器选区,一次撤销恢复全部 // 入参: page 页面索引, paths 完整对象路径, points 页面毫米多边形 // 返回: error 路径或编辑错误 func (e *Editor) EraseTargetsPath(page int, paths []ObjectPath, points []Point) error { return e.editTargets(paths, func(objects, annotations []string) error { return e.EraseSelectionPath(page, objects, annotations, points) }, func(path ObjectPath, indexes []int) error { return e.EraseCompositeObjectsPath(page, path, indexes, points) }) } // CopyTargetsStyle 原子复制样式至跨容器选区,保留各对象内容及位置 // 入参: page 页面索引, paths 完整对象路径, source 样式来源 // 返回: error 路径或样式错误 func (e *Editor) CopyTargetsStyle(page int, paths []ObjectPath, source GraphicObject) error { return e.editTargets(paths, func(objects, annotations []string) error { if err := e.CopyStyle(page, objects, source); err != nil { return err } return e.editSelectionAnnotations(page, annotations, func(path ObjectPath, indexes []int) error { return e.CopyCompositeStyle(page, path, indexes, source) }) }, func(path ObjectPath, indexes []int) error { return e.CopyCompositeStyle(page, path, indexes, source) }) } // targetGeometry 度量跨容器选区,保留输入顺序及独立宽高能力 // 入参: page 页面索引, paths 完整对象路径 // 返回: []Box 对象范围, bool 可独立缩放宽高, Box 页面范围, error 度量错误 func (e *Editor) targetGeometry(page int, paths []ObjectPath) ([]Box, bool, Box, error) { if _, _, _, err := editorTargetGroups(paths); err != nil { return nil, false, Box{}, err } _, _, paper, err := e.selectionGeometry(page, nil, nil) if err != nil { return nil, false, Box{}, err } boxes := make([]Box, len(paths)) stretch := true for i, path := range paths { if len(path.Children) != 0 { index := path.Children[len(path.Children)-1] path.Children = path.Children[:len(path.Children)-1] member, err := e.CompositeObject(page, path, index) if err != nil { return nil, false, Box{}, err } boxes[i] = member.Bounds stretch = stretch && member.Capabilities.Stretch } else { var objects, annotations []string if path.Annotation != "" { annotations = []string{path.Annotation} } else { objects = []string{path.ID} capability, err := e.ObjectCapabilities(page, path.ID) if err != nil { return nil, false, Box{}, err } stretch = stretch && capability.Stretch } boxes[i], err = e.SelectionBounds(page, objects, annotations) if err != nil { return nil, false, Box{}, err } } } return boxes, stretch, paper, nil } // TargetsBounds 获取跨容器选区的共同可见范围 // 入参: page 页面索引, paths 完整对象路径 // 返回: Box 页面毫米范围, error 路径或度量错误 func (e *Editor) TargetsBounds(page int, paths []ObjectPath) (Box, error) { objects, annotations, groups, err := editorTargetGroups(paths) if err != nil { return Box{}, err } if len(groups) == 0 { return e.SelectionBounds(page, objects, annotations) } boxes, _, _, err := e.targetGeometry(page, paths) if err != nil { return Box{}, err } var bounds Box for _, box := range boxes { bounds = unionTextBox(bounds, box) } if bounds.W <= 0 || bounds.H <= 0 { return Box{}, fmt.Errorf("selection has no visible bounds") } return bounds, nil } // orientTargets 绕跨容器选区中心应用旋转或镜像 // 入参: page 页面索引, paths 完整对象路径, matrix 方向变换 // 返回: error 路径或变换错误 func (e *Editor) orientTargets(page int, paths []ObjectPath, matrix Matrix) error { bounds, err := e.TargetsBounds(page, paths) if err != nil { return err } x, y := bounds.X+bounds.W/2, bounds.Y+bounds.H/2 return e.TransformTargets(page, paths, TranslationMatrix(x, y).Multiply(matrix).Multiply(TranslationMatrix(-x, -y))) } // RotateTargets 绕共同中心旋转跨容器选区 // 入参: page 页面索引, paths 完整对象路径, degrees 顺时针直角角度 // 返回: error 路径或变换错误 func (e *Editor) RotateTargets(page int, paths []ObjectPath, degrees int) error { matrix, err := editorRotation(degrees) if err != nil { return err } return e.orientTargets(page, paths, matrix) } // FlipTargets 绕共同中心镜像跨容器选区 // 入参: page 页面索引, paths 完整对象路径, axis 为horizontal或vertical // 返回: error 路径或变换错误 func (e *Editor) FlipTargets(page int, paths []ObjectPath, axis string) error { matrix, err := editorFlip(axis) if err != nil { return err } return e.orientTargets(page, paths, matrix) } // ResizeTargets 按共同范围调整跨容器选区,不重排文字 // 入参: page 页面索引, paths 完整对象路径, box 页面毫米目标范围 // 返回: error 路径、范围或编辑错误 func (e *Editor) ResizeTargets(page int, paths []ObjectPath, box Box) error { objects, annotations, groups, err := editorTargetGroups(paths) if err != nil { return err } if len(groups) == 0 { return e.ResizeSelection(page, objects, annotations, box) } if err := validateCreationBox(box); err != nil { return err } boxes, stretch, _, err := e.targetGeometry(page, paths) if err != nil { return err } var bounds Box for _, member := range boxes { bounds = unionTextBox(bounds, member) } if bounds.W <= 0 || bounds.H <= 0 { return fmt.Errorf("selection has no visible bounds") } sx, sy := box.W/bounds.W, box.H/bounds.H if math.Abs(sx-sy) > 1e-9*math.Max(sx, sy) { if !stretch { return fmt.Errorf("nonuniform resizing requires images or image-only groups") } } else { sy = sx } return e.TransformTargets(page, paths, Matrix{a: sx, d: sy, e: box.X - bounds.X*sx, f: box.Y - bounds.Y*sy}) } // ArrangeTargets 对齐或等距分布跨容器选区,单选对齐页面 // 入参: page 页面索引, paths 按绘制顺序排列的完整路径, distribute 是否分布, direction 对齐方向或分布轴 // 返回: error 路径、方向或编辑错误 func (e *Editor) ArrangeTargets(page int, paths []ObjectPath, distribute bool, direction string) error { objects, annotations, groups, err := editorTargetGroups(paths) if err != nil { return err } if len(groups) == 0 { if distribute { return e.DistributeSelection(page, objects, annotations, direction) } return e.AlignSelection(page, objects, annotations, direction) } if distribute && direction != "horizontal" && direction != "vertical" || !distribute && !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, direction) { return fmt.Errorf("invalid arrangement direction %q", direction) } boxes, _, target, err := e.targetGeometry(page, paths) if err != nil { return err } matrices := make([]Matrix, len(paths)) if distribute { positions := make([]editorObjectPosition, len(paths)) for i := range positions { positions[i].index = i } matrices, err = editorDistribution(boxes, positions, direction) if err != nil { return err } } else { if len(paths) > 1 { target = Box{} for _, box := range boxes { target = unionTextBox(target, box) } } for i, box := range boxes { matrices[i] = editorAlignment(box, target, direction) } } return e.Transaction(func(edit *Editor) error { for i, matrix := range matrices { if matrix != IdentityMatrix { if err := edit.TransformTargets(page, paths[i:i+1], matrix); err != nil { return err } } } return nil }) } // StyleTargetsText 原子修改跨容器文字样式,保留各自定位及组合结构 // 入参: page 页面索引, paths 完整文字路径, style 字体、字号或颜色增量 // 返回: error 路径、文字或样式错误 func (e *Editor) StyleTargetsText(page int, paths []ObjectPath, style TextStyle) error { patch := TextFormatPatch{} if style.Font != "" { patch.Font = &style.Font } if style.Size != 0 { patch.Size = &style.Size } if style.Color != "" { patch.Color = &style.Color } return e.formatTargetsText(page, paths, patch, style.Reflow) } // FormatTargetsText 原子修改普通文字和富文本的指定格式,保留未指定的混排属性 // 入参: page 页面索引, paths 完整文字路径, patch 格式增量 // 返回: error 路径、格式或不安全重排错误 func (e *Editor) FormatTargetsText(page int, paths []ObjectPath, patch TextFormatPatch) error { return e.formatTargetsText(page, paths, patch, false) } // formatTargetsText 统一文字选区格式事务,普通字体和颜色修改沿用定位保留路径 // 入参: page 页面索引, paths 完整路径, patch 格式增量, reflow 显式重排 // 返回: error 校验或编辑错误 func (e *Editor) formatTargetsText(page int, paths []ObjectPath, patch TextFormatPatch, reflow bool) error { style := TextStyle{Reflow: reflow} if patch.Font != nil { if strings.TrimSpace(*patch.Font) == "" { return fmt.Errorf("text requires a font") } style.Font = *patch.Font } if patch.Size != nil { if !finite(*patch.Size) || *patch.Size <= 0 { return fmt.Errorf("invalid text size") } style.Size = *patch.Size } if patch.Color != nil { style.Color = *patch.Color if style.Color == "" { style.Color = "0 0 0" } } decorated := patch.Bold != nil || patch.Italic != nil || patch.Underline != nil || patch.Strikeout != nil || patch.Shadow != nil || patch.Background != nil return e.editTargets(paths, func(ids, annotations []string) error { if len(annotations) != 0 { return fmt.Errorf("annotation is not a text object") } objects, _, err := e.selectedObjects(page, ids) if err != nil { return err } var plain []string for _, object := range objects { id := editorObjectID(object) if object.Type == "TextObject" && !decorated { plain = append(plain, id) continue } value, err := e.richTextObject(page, object) if err != nil { return err } if err := value.ApplyFormat(0, value.Len(), patch); err != nil { return err } if err := e.UpdateRichText(page, id, value); err != nil { return err } } if len(plain) != 0 { return e.StyleText(page, plain, style) } return nil }, func(path ObjectPath, indexes []int) error { members, err := e.CompositeObjectsAt(page, path, indexes) if err != nil { return err } var plain []int for i, member := range members { if member.Object.Type == "TextObject" && !decorated { plain = append(plain, indexes[i]) continue } if err := e.formatCompositeText(page, path, indexes[i], patch); err != nil { return err } } if len(plain) != 0 { return e.StyleCompositeText(page, path, plain, style) } return nil }) } // WriteTargetsTo 将选区及其关联资源写成独立单页OFD,来源关闭后仍可导入 // 不修改来源、历史或组合结构,加密来源须显式允许明文导出 // 不包含页面模板和签名,指向其他页面的跳转随单页导出移除 // 入参: ctx 取消上下文, writer 输出流, page 来源页, paths 完整对象路径, allowDecrypted 允许明文导出 // 返回: int64 写入字节数, error 路径、资源或写入错误 func (e *Editor) WriteTargetsTo(ctx context.Context, writer io.Writer, page int, paths []ObjectPath, allowDecrypted bool) (int64, error) { if err := ctx.Err(); err != nil { return 0, err } if e.encryption != nil && !allowDecrypted { return 0, ErrEncryptionPolicyRequired } if len(paths) == 0 { return 0, fmt.Errorf("no objects selected") } snapshot := e.transactionSnapshot() snapshot.history, snapshot.historyIndex, snapshot.historyLimit = nil, 0, 0 snapshot.transactionChanges = nil for id, origin := range snapshot.origins { if origin != nil && origin.editor == e { copy := *origin copy.editor = &snapshot snapshot.origins[id] = © } } selection, err := snapshot.CaptureTargets(page, paths) if err != nil { return 0, err } content, err := snapshot.page(page) if err != nil { return 0, err } objects, annotations, err := snapshot.PasteSelection(page, selection, 0, 0) if err != nil { return 0, err } box, err := creationBox(content.Area.PhysicalBox) if err != nil { return 0, err } packet := NewEditor() packet.SetRenderBackends(e.backends) packet.SetResources(e.runtimeResources) packet.SetFontDirs(e.fontDirs...) packet.SetFontFS(e.fontFS...) packet.OnWriteProgress = e.OnWriteProgress if _, err := packet.AddPage(box.W, box.H); err != nil { return 0, err } packet.pages[0].Area = content.Area if _, _, err := packet.importSelection(ctx, &snapshot, page, objects, annotations, 0, ObjectImportOptions{AllowDecrypted: true, OnProgress: e.OnWriteProgress}, false); err != nil { return 0, err } return packet.WriteToWithOptions(ctx, writer, WriteOptions{}) }