feat(编辑能力): 完善内部对象编辑,修复文字重框与复制异常
build.yaml / build (push) Successful in 39s

This commit is contained in:
xiaoqidun committed 2026-09-17 20:27:58 +08:00
1 parent 0f86c8d727
commit f8eeaad9f2
16 files changed
+544 -107

No files matched your search

+1 -1
View File
@@ -111,7 +111,7 @@
<button id="drawLineButton" class="button" type="button" aria-label="绘制直线" aria-pressed="false">直线</button>
<button id="drawRectangleButton" class="button" type="button" aria-label="绘制矩形" aria-pressed="false">矩形</button>
<button id="drawEllipseButton" class="button" type="button" aria-label="绘制椭圆" aria-pressed="false">椭圆</button>
<select id="arrowTool" aria-label="绘制箭头" title="绘制箭头"><option value="" hidden>箭头</option><option value="arrow">单向</option><option value="double-arrow">双向</option></select>
<select id="arrowTool" aria-label="箭头" title="箭头"><option value="" hidden>箭头</option><option value="line">无</option><option value="arrow">单向</option><option value="double-arrow">双向</option></select>
<span>插入</span>
</div>
<div class="editor-group">
+12 -4
View File
@@ -379,7 +379,7 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
onErase: (items, box, points) => {
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
const item = { index: items[0].index, id: items.map(item => item.id) };
const item = { index: items[0].index, id: items.map(item => item.id), scoped: items.every(item => item.scoped) };
if (points) return changeDocument("ofdgoEraseObjectsPath", item, points);
return box ? changeDocument("ofdgoEraseObjects", item, box.x, box.y, box.width, box.height)
: changeDocument("ofdgoDeleteObjects", item);
@@ -570,6 +570,7 @@ el.resetCropButton.addEventListener("click", () => {
}
const item = canvasEditor.selected;
if (item?.scoped) return changeDocument("ofdgoResetCompositeCrop", item);
if (canEditObject(item, "resetCrop")) return changeDocument("ofdgoResetImageCrop", item);
if (item?.imageBounds) return canvasEditor.options.onCrop(item, item.imageBounds);
});
editorClick(el.undoButton, () => changeDocument("ofdgoUndo"));
@@ -661,6 +662,12 @@ el.arrowTool.addEventListener("change", async () => {
const tool = el.arrowTool.value;
el.arrowTool.value = "";
if (await canvasEditor.commitText() && await canvasEditor.commitCrop()) {
const item = canvasEditor.selected;
if (lineShape(item?.shape)) {
const box = item.geometry;
await changeDocument("ofdgoReshapeLine", item, box.x, box.y, box.width, box.height, tool);
return;
}
setPan(false);
state.selectObjects = true;
canvasEditor.setTool(tool);
@@ -1828,7 +1835,7 @@ function updateDrawingControls() {
button.disabled = disabled || !pageCan("insert");
button.setAttribute("aria-pressed", String(tool === name));
}
el.eraseButton.disabled = el.eraseMode.disabled = disabled || Boolean(state.composite);
el.eraseButton.disabled = el.eraseMode.disabled = disabled;
el.eraseButton.setAttribute("aria-pressed", String(tool.startsWith("erase-")));
el.selectObjectButton.setAttribute("aria-pressed", String(canvasEditor.enabled && !tool));
const styleDisabled = disabled || (canvasEditor.selected ? !path || !canEditObject(path, "paint") : !pageCan("insert"));
@@ -1860,6 +1867,7 @@ async function changeDocument(name, item, ...args) {
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoCropImage: "crop", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath",
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order" }[name];
const members = item?.items || (item ? [item] : []);
const scoped = scope && members.length && members.every(member => member.scoped);
@@ -1876,7 +1884,7 @@ async function changeDocument(name, item, ...args) {
return true;
}
const view = name === "ofdgoUndo" ? state.editorViews.get(revision) : name === "ofdgoRedo" ? state.editorViews.get(doc.revision) : null;
const reindex = !!scope && (scoped && ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoCopyObjects", "ofdgoOrderObjects"].includes(name)
const reindex = !!scope && (scoped && ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoCopyObjects", "ofdgoOrderObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name)
|| inserting || name === "ofdgoPasteObjects" && !!args[4]);
const reindexed = restoring ? view?.reindex && view.before : reindex && before;
const clipboard = state.objectClipboard;
@@ -5616,7 +5624,7 @@ function updateObjectControls(item, reset = false) {
el.cropImageButton.setAttribute("aria-pressed", String(cropping));
el.resetCropButton.textContent = cropping ? "取消" : "还原";
el.resetCropButton.setAttribute("aria-label", cropping ? "取消裁剪" : "还原图片");
el.resetCropButton.disabled = el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? disabled : el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
+3 -1
View File
@@ -289,7 +289,7 @@ export class CanvasEditor {
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("preserveAspectRatio", "none");
let contour;
if (object.type === "PathObject" && !object.scoped) {
if (object.type === "PathObject" && (!object.scoped || object.shape)) {
contour = document.createElementNS("http://www.w3.org/2000/svg", object.shape === "line" ? "line" : object.shape === "ellipse" ? "ellipse" : object.shape === "rectangle" ? "rect" : "path");
if (!object.shape) contour.setAttribute("d", object.outline);
svg.append(contour);
@@ -1025,6 +1025,7 @@ export class CanvasEditor {
this.input = { input, item, face, fontChoice: item.fontChoice };
const editing = this.input;
item.artwork?.classList.add("edit-text-source");
item.node.classList.add("edit-text-source");
item.surface.append(input);
input.addEventListener("input", () => this.options.onTextChange());
input.addEventListener("compositionstart", () => { editing.composing = true; });
@@ -1143,6 +1144,7 @@ export class CanvasEditor {
if (editing) {
editing.input.remove();
editing.item.artwork?.classList.remove("edit-text-source");
editing.item.node.classList.remove("edit-text-source");
document.fonts.delete(editing.face);
if (editing.item.draft && this.selected === editing.item) this.select(null);
this.options.onTextChange();
+58 -5
View File
@@ -136,6 +136,8 @@ func RunWASM() {
registerCallback("ofdgoAlignObject", alignObject)
registerCallback("ofdgoTransformObject", transformObject)
registerCallback("ofdgoReshapeObject", reshapeObject)
registerCallback("ofdgoReshapeLine", reshapeLine)
registerCallback("ofdgoResetImageCrop", resetImageCrop)
registerCallback("ofdgoLayoutText", layoutText)
registerCallback("ofdgoTransformObjects", transformObjects)
registerCallback("ofdgoCompositeObjects", compositeObjects)
@@ -833,7 +835,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
// 返回: map[string]any 前端能力
func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[string]any {
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
if missing := capability.MissingGlyphs; missing != nil {
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
}
@@ -923,6 +925,9 @@ func compositeObjects(args []js.Value) (any, error) {
item := map[string]any{"id": fmt.Sprintf("%s/%d", key, i), "type": member.Object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": i, "position": member.Position.Index, "count": member.Position.Count, "container": key + ":" + member.Position.Container, "scoped": true, "contours": member.Contours,
"capabilities": editorCapabilities(member.Capabilities, member.Object.Type)}
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
if kind, geometry := member.Shape(); kind != "" {
item["shape"], item["geometry"] = string(kind), editorBox(geometry)
}
if member.Object.Type == "TextObject" {
object := member.Object.TextObject
value, layout := object.TextLayout()
@@ -978,6 +983,19 @@ func changeCompositeObjects(args []js.Value) (any, error) {
page := args[0].Int()
operation := args[3].String()
switch operation {
case "reshape", "line":
if len(indexes) != 1 {
return fmt.Errorf("line editing requires one member")
}
kind := ofdgo.ShapeKind("")
if operation == "line" {
kind = ofdgo.ShapeKind(args[8].String())
}
return currentEditor.ReshapeCompositeLine(page, path, indexes[0], kind, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
case "erase":
return currentEditor.EraseCompositeObjects(page, path, indexes, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
case "erasePath":
return currentEditor.EraseCompositeObjectsPath(page, path, indexes, pointsFromJS(args[4]))
case "delete":
return currentEditor.DeleteCompositeObjects(page, path, indexes)
case "copy":
@@ -1265,6 +1283,27 @@ func reshapeObject(args []js.Value) (any, error) {
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
// reshapeLine 修改直线端点与箭头类型,复用库层样式保留逻辑
// 入参: args 页码、对象标识、页面端点及箭头类型
// 返回: any 文档信息, error 错误信息
func reshapeLine(args []js.Value) (any, error) {
return changeObjects(func() error {
page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id)
if err != nil {
return err
}
if object.Type != "PathObject" {
return fmt.Errorf("object is not a path")
}
object.PathObject, err = object.PathObject.ReshapeLine(ofdgo.ShapeKind(args[6].String()), ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: args[4].Float(), H: args[5].Float()})
if err != nil {
return err
}
return currentEditor.UpdateObject(page, id, object)
})
}
// copyObjects 复制选区并返回新对象标识,复用字体和图片资源
// 入参: args 页码、对象标识列表和位移
// 返回: any 文档信息, error 错误信息
@@ -2164,6 +2203,13 @@ func cropImage(args []js.Value) (any, error) {
})
}
// resetImageCrop 还原跨范围复制图片的会话裁剪,不移除原始及父级裁剪
// 入参: args 页面索引及对象标识
// 返回: any 文档信息, error 错误信息
func resetImageCrop(args []js.Value) (any, error) {
return changeObjects(func() error { return currentEditor.ResetImageCrop(args[0].Int(), args[1].String()) })
}
// fitImage 按原始比例适应或填充图片框
// 入参: args 页面索引、对象标识和适应方式
// 返回: any 文档信息, error 错误信息
@@ -2249,13 +2295,20 @@ func deleteObjects(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息
func eraseObjectsPath(args []js.Value) (any, error) {
return changeObjects(func() error {
points := make([]ofdgo.Point, args[2].Length())
return currentEditor.EraseObjectsPath(args[0].Int(), stringsFromJS(args[1]), pointsFromJS(args[2]))
})
}
// pointsFromJS 读取前端页面坐标点数组
// 入参: value 坐标点数组
// 返回: []ofdgo.Point 页面毫米坐标
func pointsFromJS(value js.Value) []ofdgo.Point {
points := make([]ofdgo.Point, value.Length())
for i := range points {
point := args[2].Index(i)
point := value.Index(i)
points[i] = ofdgo.Point{X: point.Get("x").Float(), Y: point.Get("y").Float()}
}
return currentEditor.EraseObjectsPath(args[0].Int(), stringsFromJS(args[1]), points)
})
return points
}
// eraseObjects 按页面矩形范围擦除对象,保留局部裁剪之外的内容
+4 -4
View File
@@ -858,13 +858,13 @@ func (e *Editor) validateObjectClips(clips *Clips) error {
func cloneEditorObject(object GraphicObject) (GraphicObject, error) {
switch object.Type {
case "TextObject":
return GraphicObject{Type: object.Type, TextObject: cloneEditorData(object.TextObject)}, nil
return GraphicObject{Type: object.Type, TextObject: cloneEditorData(object.TextObject), state: object.state}, nil
case "PathObject":
return GraphicObject{Type: object.Type, PathObject: cloneEditorData(object.PathObject)}, nil
return GraphicObject{Type: object.Type, PathObject: cloneEditorData(object.PathObject), state: object.state}, nil
case "ImageObject":
return GraphicObject{Type: object.Type, ImageObject: cloneEditorData(object.ImageObject)}, nil
return GraphicObject{Type: object.Type, ImageObject: cloneEditorData(object.ImageObject), state: object.state}, nil
case "CompositeObject", "CompositeGraphicUnit":
return GraphicObject{Type: object.Type, CompositeGraphicUnit: cloneEditorData(object.CompositeGraphicUnit)}, nil
return GraphicObject{Type: object.Type, CompositeGraphicUnit: cloneEditorData(object.CompositeGraphicUnit), state: object.state}, nil
}
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
}
+21 -9
View File
@@ -72,16 +72,16 @@ type editorCompositeNode struct {
states map[string]editorCompositeState
}
// editorCompositeState 保存内部对象的会话排版及新增裁剪起点,不写入OFD
// editorCompositeState 保存内部对象的会话排版及可还原裁剪,不写入OFD
type editorCompositeState struct {
layout *textLayout
crop *editorCompositeCrop
origin [2]float64
}
// editorCompositeCrop 记录编辑前的裁剪数量及节点存在性
// editorCompositeCrop 记录本次编辑新增的裁剪序号及原节点存在性
type editorCompositeCrop struct {
count int
indexes []int
exists bool
}
@@ -387,6 +387,9 @@ func (e *Editor) compositeMembers(n *editorCompositeNode, reader *Reader, render
var collect func(*editorXML) error
collect = func(container *editorXML) error {
for _, child := range container.children {
if child.name.Space != "" && child.name.Space != ofdNamespace && child.name.Space != "http://www.ofdspec.org" {
continue
}
switch child.name.Local {
case "Content", "PageBlock":
if err := collect(child); err != nil {
@@ -492,6 +495,7 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
clips := node.object.ImageObject.Clips
_, capability.CropImage = node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
capability.FitImage = capability.CropImage && axisAlignedMatrix(NewMatrix(node.object.ImageObject.CTM))
capability.ResetCrop = node.states[editorObjectID(node.object)].crop != nil
}
_, ctm := editorGeometry(node.object)
matrix := node.parent.Multiply(NewMatrix(ctm))
@@ -651,22 +655,19 @@ func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int
// 返回: error 错误信息
func (e *Editor) editCompositeObjects(page int, path ObjectPath, indexes []int, edit func(*Renderer, []*editorCompositeNode, []CompositeMember) error) (err error) {
return e.editCompositeScope(page, path, func(renderer *Renderer, _ *editorCompositeNode, nodes []*editorCompositeNode) error {
members := e.measureCompositeMembers(renderer, nodes)
selected := make(map[int]bool)
var selectedNodes []*editorCompositeNode
var selectedMembers []CompositeMember
for _, i := range indexes {
if i < 0 || i >= len(nodes) || selected[i] {
return fmt.Errorf("invalid composite selection")
}
selected[i] = true
selectedNodes = append(selectedNodes, nodes[i])
selectedMembers = append(selectedMembers, members[i])
}
if len(selected) == 0 {
return nil
}
return edit(renderer, selectedNodes, selectedMembers)
return edit(renderer, selectedNodes, e.measureCompositeMembers(renderer, selectedNodes))
})
}
@@ -929,10 +930,21 @@ func (e *Editor) addCompositeResource(data []byte, states map[string]editorCompo
// 入参: id 资源标识, data 资源XML
// 返回: editorResource 资源, error 错误信息
func (e *Editor) compositeResource(id string, data []byte) (editorResource, error) {
root, err := parseEditorXML(data)
refs, err := editorResourceReferences(data)
if err != nil {
return editorResource{}, err
}
return editorResource{name: e.resourceDirectory() + "/Composite_" + id + ".xml", data: data, composite: id, references: refs}, nil
}
// editorResourceReferences 收集标准资源XML中的对象引用
// 入参: data 资源XML
// 返回: []string 引用标识, error 错误信息
func editorResourceReferences(data []byte) ([]string, error) {
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
refs := make(map[string]bool)
var collect func(*editorXML)
collect = func(node *editorXML) {
@@ -948,7 +960,7 @@ func (e *Editor) compositeResource(id string, data []byte) (editorResource, erro
}
}
collect(root)
return editorResource{name: e.resourceDirectory() + "/Composite_" + id + ".xml", data: data, composite: id, references: slices.Sorted(maps.Keys(refs))}, nil
return slices.Sorted(maps.Keys(refs)), nil
}
// collectCompositeReferences 收集内联复合对象引用的新资源
+122 -38
View File
@@ -37,8 +37,7 @@ type CompositeSelection struct {
source *editorSourcePage
}
// PasteCompositeSelection 将内部基本对象快照粘贴到页面顶层,保持源继承外观和裁剪
// 复合成员需留在原范围操作,不将未知嵌套结构强制扁平化
// PasteCompositeSelection 将内部对象快照粘贴到页面顶层,保持嵌套结构、源继承外观和裁剪
// 入参: page 目标页, selection 当前编辑器内捕获的快照, dx、dy 页面位移
// 返回: []string 新对象标识, error 错误信息
func (e *Editor) PasteCompositeSelection(page int, selection *CompositeSelection, dx, dy float64) ([]string, error) {
@@ -48,31 +47,35 @@ func (e *Editor) PasteCompositeSelection(page int, selection *CompositeSelection
if _, err := e.page(page); err != nil {
return nil, err
}
objects, err := e.compositeSelectionObjects(selection)
if err != nil {
return nil, err
}
return e.CopyObjects(page, objects, dx, dy)
var result []string
err := e.pasteCompositeSelection(selection, func(objects []GraphicObject) error {
var err error
result, err = e.CopyObjects(page, objects, dx, dy)
return err
})
return result, err
}
// compositeSelectionObjects 固定内部基本对象的页面几何和父裁剪,保留原XML扩展
// 入参: selection 独立内部快照
// 返回: []GraphicObject 可复制到其他范围的快照, error 错误信息
func (e *Editor) compositeSelectionObjects(selection *CompositeSelection) ([]GraphicObject, error) {
// pasteCompositeSelection 固定内部快照的页面几何和外观,粘贴失败时回收新增绘制资源
// 入参: selection 独立内部快照, paste 目标范围粘贴操作
// 返回: error 错误信息
func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste func([]GraphicObject) error) (err error) {
if selection == nil || selection.editor != e {
return nil, fmt.Errorf("composite selection belongs to another editor")
}
for _, node := range selection.nodes {
if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" {
return nil, fmt.Errorf("composite groups require their original editing scope")
return fmt.Errorf("composite selection belongs to another editor")
}
count, maximum, ready := len(e.resources), e.maxID, e.source.idsReady
defer func() {
if err != nil {
clear(e.resources[count:])
e.resources, e.maxID, e.source.idsReady = e.resources[:count], maximum, ready
}
}()
if err := e.prepareSourceIDs(); err != nil {
return nil, err
return err
}
reader, err := e.Reader()
if err != nil {
return nil, err
return err
}
defer reader.Close()
renderer := NewRenderer(reader, WithFontFS(e.fontFS...))
@@ -80,12 +83,13 @@ func (e *Editor) compositeSelectionObjects(selection *CompositeSelection) ([]Gra
for _, source := range selection.nodes {
copy, err := newEditorCompositeNode(source.data)
if err != nil {
return nil, err
return err
}
copy.setStates(source.states)
copy.defaults = source.defaults
object, err := e.compositeMemberStyle(source)
if err != nil {
return nil, err
return err
}
object = mergeGraphicObjectAlpha(object, source.alpha)
if !source.visible || source.clip != nil && source.clip.Empty() {
@@ -97,48 +101,126 @@ func (e *Editor) compositeSelectionObjects(selection *CompositeSelection) ([]Gra
object.PathObject.Visible = &visible
case "ImageObject":
object.ImageObject.Visible = &visible
case "CompositeObject", "CompositeGraphicUnit":
object.CompositeGraphicUnit.Visible = &visible
}
}
if !source.boundaryInCTM {
object = compositeBoundary(object, source.parent, true)
}
if err := copy.update(object); err != nil {
return nil, err
return err
}
if !source.boundaryInCTM {
if err := copy.convertCoordinates(source.parent, true); err != nil {
return err
}
}
copy.parent, copy.boundaryInCTM = IdentityMatrix, true
if err := e.transformCompositeMember(renderer, copy, source.parent); err != nil {
return nil, err
return err
}
if err := e.isolateCompositeStyle(copy); err != nil {
return nil, err
return err
}
if source.clip != nil && !source.clip.Empty() {
shape := compositeClipPath(source.clip)
clips := *editorObjectClips(&copy.object)
matrix, ok := copy.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
if !ok {
return nil, fmt.Errorf("composite clip transform is not invertible")
return fmt.Errorf("composite clip transform is not invertible")
}
if err := appendCompositeClip(copy, shape, matrix, false); err != nil {
return nil, err
return err
}
}
if copy.object.Type == "CompositeObject" || copy.object.Type == "CompositeGraphicUnit" {
if err := copy.convertCoordinates(IdentityMatrix, false); err != nil {
return err
}
}
copy.node.parent = &editorXML{name: xml.Name{Local: "Layer"}, parent: &editorXML{name: xml.Name{Local: "Content"}}}
object, err = e.resolveEditorStyle(copy.object, "")
if err != nil {
return nil, err
return err
}
object.state = copy.states[editorObjectID(copy.object)]
object.TextObject.layout = object.state.layout
copy.object.TextObject.layout = object.TextObject.layout
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
object.CompositeGraphicUnit.states = maps.Clone(copy.states)
copy.object.CompositeGraphicUnit.states = object.CompositeGraphicUnit.states
}
object.origin = &editorObjectOrigin{page: selection.source, data: copy.data, node: copy.node, object: copy.object}
objects = append(objects, object)
}
return objects, nil
return paste(objects)
}
// convertCoordinates 转换内联基本成员的边界约定,引用资源保持标准局部坐标
// 入参: parent 所在范围矩阵, inward 是否转换到随父变换的坐标
// 返回: error 错误信息
func (n *editorCompositeNode) convertCoordinates(parent Matrix, inward bool) error {
if n.object.Type != "CompositeObject" && n.object.Type != "CompositeGraphicUnit" {
return n.update(compositeBoundary(n.object, parent, inward))
}
var patches []editorXMLPatch
var visit func(*editorXML, Matrix) error
visit = func(node *editorXML, matrix Matrix) error {
if node.name.Local == "CompositeObject" || node.name.Local == "CompositeGraphicUnit" {
box, _ := ParseBox(node.attr("Boundary"))
matrix = matrix.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(node.attr("CTM")))
if _, ok := matrix.Invert(); !ok {
return fmt.Errorf("composite copy requires invertible transforms")
}
}
for _, child := range node.children {
if child.name.Space != "" && child.name.Space != ofdNamespace && child.name.Space != "http://www.ofdspec.org" {
continue
}
switch child.name.Local {
case "Content", "PageBlock", "CompositeObject", "CompositeGraphicUnit":
if err := visit(child, matrix); err != nil {
return err
}
case "TextObject", "PathObject", "ImageObject":
data, err := editorXMLStandalone(n.data[child.start:child.end], child)
if err != nil {
return err
}
member, err := newEditorCompositeNode(data)
if err != nil {
return err
}
if err := member.update(compositeBoundary(cloneEditorData(member.object), matrix, inward)); err != nil {
return err
}
patches = append(patches, editorXMLPatch{child.start, child.end, member.data})
}
}
return nil
}
if err := visit(n.node, parent); err != nil {
return err
}
data := editorPatchXML(n.data, patches)
next, err := newEditorCompositeNode(data)
if err != nil {
return err
}
n.data, n.node, n.object = data, next.node, next.object
return nil
}
// compositeClipPath 将已解析的父裁剪转换为标准紧缩路径,使用页面毫米坐标
// 入参: path 画布裁剪
// 返回: PathObject 标准裁剪路径
func compositeClipPath(path *canvas.Path) PathObject {
path = path.Copy().Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}).ReplaceArcs()
return editorClipPath(path.Copy().Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}))
}
// editorClipPath 将页面坐标路径写为标准紧缩裁剪路径
// 入参: path 页面毫米路径
// 返回: PathObject 标准裁剪路径
func editorClipPath(path *canvas.Path) PathObject {
path = path.ReplaceArcs()
data := path.Data()
var value strings.Builder
for i := 0; i < len(data); {
@@ -211,7 +293,7 @@ func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []in
}
// PasteCompositeObjects 将快照粘贴至内部范围末尾,不依赖源成员当前的序号或存在性
// 同范围沿用原容器和当前继承样式,跨范围固定基本对象的源外观,位移使用页面毫米
// 同范围沿用原容器和当前继承样式,跨范围固定对象的源外观,位移使用页面毫米
// 目标范围的父透明度及裁剪仍作用于新成员,全部成员提交一次撤销记录
// 入参: page 页面索引, path 父复合路径, selection 捕获的快照, dx、dy 页面位移
// 返回: []int 新成员序号, error 错误信息
@@ -226,11 +308,13 @@ func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *Com
return nil, err
}
if e.pages[page].ID != selection.page || path.ID != selection.path.ID || !slices.Equal(path.Children, selection.path.Children) {
objects, err := e.compositeSelectionObjects(selection)
if err != nil {
return nil, err
}
return e.CopyObjectsToComposite(page, path, objects, dx, dy)
var result []int
err := e.pasteCompositeSelection(selection, func(objects []GraphicObject) error {
var err error
result, err = e.CopyObjectsToComposite(page, path, objects, dx, dy)
return err
})
return result, err
}
var result []int
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, _ []*editorCompositeNode) error {
@@ -321,7 +405,7 @@ func (n *editorCompositeNode) loadedScope(path []int) *editorCompositeNode {
func compositeContainers(node *editorXML) []*editorXML {
var result []*editorXML
for _, child := range node.children {
if child.name.Local == "Content" || child.name.Local == "PageBlock" {
if (child.name.Local == "Content" || child.name.Local == "PageBlock") && (child.name.Space == "" || child.name.Space == ofdNamespace || child.name.Space == "http://www.ofdspec.org") {
result = append(result, child)
}
}
+68 -11
View File
@@ -18,6 +18,8 @@ import (
"bytes"
"encoding/xml"
"fmt"
"reflect"
"slices"
)
// matrix 获取对象局部坐标到页面的变换,兼容旧式内联边界
@@ -33,7 +35,7 @@ func (n *editorCompositeNode) matrix(transform bool) Matrix {
m = Matrix{a: box.W, d: box.H}
}
}
if n.boundaryInCTM {
if n.boundaryInCTM || n.object.Type == "CompositeObject" || n.object.Type == "CompositeGraphicUnit" {
return n.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(m)
}
if !transform {
@@ -74,19 +76,29 @@ func (e *Editor) CropCompositeImage(page int, path ObjectPath, index int, box Bo
})
}
// appendCompositeClip 追加独立交集裁剪并记录会话还原起点,保留原始裁剪XML
// 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原起点
// appendCompositeClip 追加独立交集裁剪并记录可还原序号,保留原始裁剪XML
// 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原序号
// 返回: error 错误信息
func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Matrix, track bool) error {
state := node.states[editorObjectID(node.object)]
if track && state.crop == nil {
state.crop = &editorCompositeCrop{exists: node.node.child("Clips") != nil}
if clips := node.object.ImageObject.Clips; clips != nil {
state.crop.count = len(clips.Clip)
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{{Area: []ClipArea{{CTM: matrix.String(), Path: []PathObject{shape}}}}}})
x.clips(&Clips{Clip: []Clip{clip}})
})
if err != nil {
return err
@@ -130,6 +142,51 @@ func (e *Editor) ResetCompositeImageCrop(page int, path ObjectPath, index int) e
})
}
// 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})
}
// resetCompositeImageCrop 恢复图片原始裁剪节点,不还原后续变换和资源替换
// 入参: node 图片节点
// 返回: error 错误信息
@@ -143,13 +200,13 @@ func resetCompositeImageCrop(node *editorCompositeNode) error {
}
clips := node.node.child("Clips")
var patches []editorXMLPatch
if !state.crop.exists {
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 count >= state.crop.count {
if slices.Contains(state.crop.indexes, count) {
patches = append(patches, editorXMLPatch{child.start, child.end, nil})
}
count++
+85 -25
View File
@@ -19,11 +19,12 @@ import (
"encoding/xml"
"fmt"
"image/color"
"reflect"
"strconv"
)
// AddCompositeObject 按页面坐标向内部范围末尾添加基本对象,复用已注册资源
// 入参: page 页面索引, path 父路径, object 文字、图片或路径
// AddCompositeObject 按页面坐标向内部范围末尾添加对象,复用已注册资源
// 入参: page 页面索引, path 父路径, object 基本对象或复合快照
// 返回: int 新成员序号, error 错误信息
func (e *Editor) AddCompositeObject(page int, path ObjectPath, object GraphicObject) (int, error) {
indexes, err := e.CopyObjectsToComposite(page, path, []GraphicObject{object}, 0, 0)
@@ -64,9 +65,6 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
}
var content []byte
for _, source := range objects {
if source.Type == "CompositeObject" || source.Type == "CompositeGraphicUnit" {
return fmt.Errorf("composite groups require their original editing scope")
}
id := e.nextID()
object, err := e.prepareCopiedObject(id, source)
if err != nil {
@@ -95,6 +93,16 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
return err
}
node.parent, node.boundaryInCTM = IdentityMatrix, true
node.setStates(object.CompositeGraphicUnit.states)
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
if err := node.convertCoordinates(IdentityMatrix, true); err != nil {
return err
}
node.defaults, err = e.editorDrawParam(e.copiedLayerStyle(source), make(map[string]bool))
if err != nil {
return err
}
}
if err := e.isolateCompositeStyle(node); err != nil {
return err
}
@@ -102,14 +110,16 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
return err
}
if !owner.boundaryInCTM {
if err := node.update(compositeBoundary(node.object, parent, false)); err != nil {
if err := node.convertCoordinates(parent, false); err != nil {
return err
}
}
if origin == nil && object.TextObject.layout != nil {
state := object.state
if object.TextObject.layout != nil && !reflect.DeepEqual(state.layout, object.TextObject.layout) {
y, _ := strconv.ParseFloat(object.TextObject.TextCode[0].Y, 64)
node.setState(editorCompositeState{layout: object.TextObject.layout, origin: [2]float64{0, y}})
state.layout, state.origin = object.TextObject.layout, [2]float64{0, y}
}
node.setState(state)
for id, state := range node.states {
owner.states[id] = state
}
@@ -142,10 +152,39 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
return result, nil
}
// isolateCompositeStyle 固定基本对象的绘制参数,避免目标容器改变颜色、虚线和线帽
// isolateCompositeStyle 固定对象的绘制参数,避免目标容器改变颜色、虚线和线帽
// 入参: node 页面坐标中的对象节点
// 返回: error 错误信息
func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" {
own, err := e.editorDrawParam(node.object.CompositeGraphicUnit.DrawParam, make(map[string]bool))
if err != nil {
return err
}
if node.defaults != nil {
own = mergeDrawParam(*node.defaults, own)
}
for _, color := range []*FillColor{own.FillColor, (*FillColor)(own.StrokeColor)} {
if err := e.editorColor(color); err != nil {
return &EditError{Code: EditUnsupportedColor, Err: fmt.Errorf("composite inherited color cannot be isolated: %w", err)}
}
}
id, err := e.neutralDrawParam()
if err != nil {
return err
}
base, err := e.editorDrawParam(id, make(map[string]bool))
if err != nil {
return err
}
id, err = e.addEditorDrawParam(*mergeDrawParam(*base, own))
if err != nil {
return err
}
object := node.object
object.CompositeGraphicUnit.DrawParam = id
return node.update(object)
}
object, err := e.resolveEditorStyleDefaults(node.object, &DrawParam{})
if err != nil {
return err
@@ -168,31 +207,51 @@ func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
// neutralDrawParam 注册可复用的完整默认绘制参数,不改变文档默认颜色空间
// 返回: string 绘制参数标识, error 错误信息
func (e *Editor) neutralDrawParam() (string, error) {
for _, resource := range e.resources {
if resource.draw != nil {
return resource.draw.ID, nil
}
}
if err := e.prepareSourceIDs(); err != nil {
return "", err
}
fill, err := e.RGBColor(color.NRGBA{A: 255})
if err != nil {
return "", err
}
zero := 0.0
draw := &DrawParam{ID: e.nextID(), LineWidth: defaultPathLineWidth, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, DashOffset: &zero, dashPatternSet: true, FillColor: fill, StrokeColor: (*StrokeColor)(fill)}
return e.addEditorDrawParam(DrawParam{LineWidth: defaultPathLineWidth, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, DashOffset: &zero, dashPatternSet: true, FillColor: fill, StrokeColor: (*StrokeColor)(fill)})
}
// addEditorDrawParam 注册完整有效绘制参数,相同外观复用资源
// 入参: draw 不含继承关系的有效参数
// 返回: string 资源标识, error 错误信息
func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
draw.ID, draw.Relative, draw.ResourceID, draw.BaseLoc, draw.Link = "", "", "", "", ""
for _, resource := range e.resources {
if resource.draw != nil {
previous := *resource.draw
previous.ID = ""
if reflect.DeepEqual(previous, draw) {
return resource.draw.ID, nil
}
}
}
if err := e.prepareSourceIDs(); err != nil {
return "", err
}
draw.ID = e.nextID()
data, err := encodeOFDXML(func(x *ofdXML) {
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
x.start("DrawParams", nil)
attrs := ofdAttrs{}
for _, pair := range [][2]string{{"ID", draw.ID}, {"LineWidth", ofdNumber(draw.LineWidth)}, {"Cap", draw.Cap}, {"Join", draw.Join}, {"MiterLimit", ofdNumber(draw.MiterLimit)}, {"DashOffset", "0"}} {
for _, pair := range [][2]string{{"ID", draw.ID}, {"LineWidth", ofdNumber(draw.LineWidth)}, {"Cap", draw.Cap}, {"Join", draw.Join}, {"MiterLimit", ofdNumber(draw.MiterLimit)}, {"Font", draw.Font}} {
attrs.add(pair[0], pair[1])
}
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: ""})
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern})
if draw.DashOffset != nil {
attrs.add("DashOffset", ofdNumber(*draw.DashOffset))
}
attrs.number("Size", draw.Size)
attrs.number("Weight", float64(draw.Weight))
if draw.Italic {
attrs.add("Italic", "true")
}
x.start("DrawParam", attrs)
x.color("FillColor", fill)
x.color("StrokeColor", fill)
x.color("FillColor", draw.FillColor)
x.color("StrokeColor", (*FillColor)(draw.StrokeColor))
x.end("DrawParam")
x.end("DrawParams")
x.end("Res")
@@ -200,10 +259,11 @@ func (e *Editor) neutralDrawParam() (string, error) {
if err != nil {
return "", err
}
resource := editorResource{name: e.resourceDirectory() + "/DrawParam_" + draw.ID + ".xml", data: data, draw: draw}
if fill.ColorSpace != "" {
resource.references = []string{fill.ColorSpace}
refs, err := editorResourceReferences(data)
if err != nil {
return "", err
}
resource := editorResource{name: e.resourceDirectory() + "/DrawParam_" + draw.ID + ".xml", data: data, draw: &draw, references: refs}
e.resources = append(e.resources, resource)
return draw.ID, nil
}
+65
View File
@@ -183,6 +183,71 @@ func (n *editorCompositeNode) update(object GraphicObject) error {
return nil
}
// 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 !lineShapeKind(kind) {
return "", Box{}
}
return kind, transformLineBox(box, m.lineParent())
}
// lineParent 获取直线所在坐标到页面的变换,兼容旧式内联边界
// 返回: Matrix 直线父坐标变换
func (m CompositeMember) lineParent() Matrix {
p := m.Object.PathObject
box, _ := ParseBox(p.Boundary)
inverse, _ := TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(p.CTM)).Invert()
return m.Matrix.Multiply(inverse)
}
// transformLineBox 变换起点及有符号端点位移
// 入参: box 直线端点, matrix 坐标变换
// 返回: Box 变换后的端点
func transformLineBox(box Box, matrix Matrix) Box {
x, y := matrix.Transform(box.X, box.Y)
w, h := matrixVector(matrix, box.W, box.H)
return Box{X: x, Y: y, W: w, H: h}
}
// ReshapeCompositeLine 修改内部直线的页面端点及箭头,保留原始属性和父变换
// 入参: page 页面索引, path 父路径, index 成员序号, kind 直线类型, box 页面端点
// 返回: error 错误信息
func (e *Editor) ReshapeCompositeLine(page int, path ObjectPath, index int, kind ShapeKind, box Box) error {
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
before, geometry := members[0].Shape()
if before == "" {
return fmt.Errorf("composite member is not a supported line")
}
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
object.PathObject, err = object.PathObject.ReshapeLine(kind, transformLineBox(box, inverse))
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 错误信息
+1
View File
@@ -111,6 +111,7 @@ func (e *Editor) copyObjectOrigin(before, after GraphicObject, maximum *int) (Gr
return GraphicObject{}, nil, err
}
result.TextObject.layout = after.TextObject.layout
result.state = after.state
result.CompositeGraphicUnit.states = remapCompositeStates(after.CompositeGraphicUnit.states, ids)
root.parent = origin.node.parent
return result, &editorObjectOrigin{page: origin.page, data: data, node: root, object: result}, nil
+69 -7
View File
@@ -41,13 +41,25 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
// 入参: page 页面索引, ids 对象标识, points 页面毫米坐标,至少三个点
// 返回: error 错误信息
func (e *Editor) EraseObjectsPath(page int, ids []string, points []Point) error {
region, err := editorEraseRegion(points)
if err != nil || region.Empty() {
return err
}
bounds := region.FastBounds()
return e.eraseObjects(page, ids, Box{X: bounds.X0, Y: bounds.Y0, W: bounds.W(), H: bounds.H()}, points, region)
}
// editorEraseRegion 校验闭合折线并按奇偶规则构建擦除区域
// 入参: points 页面毫米坐标
// 返回: *canvas.Path 擦除区域, error 错误信息
func editorEraseRegion(points []Point) (*canvas.Path, error) {
if len(points) < 3 {
return fmt.Errorf("erase path requires at least three points")
return nil, fmt.Errorf("erase path requires at least three points")
}
region := &canvas.Path{}
for i, point := range points {
if !finite(point.X) || !finite(point.Y) {
return fmt.Errorf("invalid erase path point %d", i)
return nil, fmt.Errorf("invalid erase path point %d", i)
}
if i == 0 {
region.MoveTo(point.X, point.Y)
@@ -56,12 +68,62 @@ func (e *Editor) EraseObjectsPath(page int, ids []string, points []Point) error
}
}
region.Close()
region = region.Settle(canvas.EvenOdd)
if region.Empty() {
return nil
return region.Settle(canvas.EvenOdd), nil
}
// EraseCompositeObjects 擦除内部成员与矩形相交的部分,保留原始属性及共享资源
// 全部覆盖时删除成员,部分覆盖追加标准裁剪,不用于敏感信息脱敏
// 入参: page 页面索引, path 父路径, indexes 成员序号, box 页面毫米范围
// 返回: error 错误信息
func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
if _, err := creationBox(editorBoxString(box)); err != nil {
return err
}
bounds := region.FastBounds()
return e.eraseObjects(page, ids, Box{X: bounds.X0, Y: bounds.Y0, W: bounds.W(), H: bounds.H()}, points, region)
return e.eraseCompositeObjects(page, path, indexes, canvas.Rectangle(box.W, box.H).Translate(box.X, box.Y))
}
// EraseCompositeObjectsPath 按闭合折线擦除内部成员,末点自动连接起点
// 与矩形擦除共用原子提交和撤销逻辑,不用于敏感信息脱敏
// 入参: page 页面索引, path 父路径, indexes 成员序号, points 页面毫米坐标
// 返回: error 错误信息
func (e *Editor) EraseCompositeObjectsPath(page int, path ObjectPath, indexes []int, points []Point) error {
region, err := editorEraseRegion(points)
if err != nil || region.Empty() {
return err
}
return e.eraseCompositeObjects(page, path, indexes, region)
}
// eraseCompositeObjects 按页面区域裁剪内部成员,全部成功后提交一次历史
// 入参: page 页面索引, path 父路径, indexes 成员序号, region 擦除区域
// 返回: error 错误信息
func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int, region *canvas.Path) error {
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
for i, node := range nodes {
box := members[i].Bounds
bounds := canvas.Rectangle(box.W, box.H).Translate(box.X, box.Y)
if box.W <= 0 || box.H <= 0 || bounds.And(region).Empty() {
continue
}
if bounds.Not(region).Empty() {
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, nil})
continue
}
if !members[i].Capabilities.Arrange {
return fmt.Errorf("composite member cannot be erased")
}
clips := *editorObjectClips(&node.object)
inverse, ok := node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
if !ok {
return fmt.Errorf("composite clip transform is not invertible")
}
outer := canvas.Rectangle(box.W+2, box.H+2).Translate(box.X-1, box.Y-1)
if err := appendCompositeClip(node, editorClipPath(outer.Not(region)), inverse, false); err != nil {
return err
}
}
return nil
})
}
// eraseObjects 按矩形或闭合路径裁剪对象,只在全部对象处理成功后提交修改
+4
View File
@@ -62,6 +62,7 @@ type editorSourcePage struct {
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
// FitImage表示可按原始图片比例适应或填充现有边界
// ResetCrop表示有可单独还原的会话裁剪
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
@@ -72,6 +73,7 @@ type ObjectCapabilities struct {
ReplaceImage bool
CropImage bool
FitImage bool
ResetCrop bool
Reflow bool
LayoutKnown bool
Transform bool
@@ -358,6 +360,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
}
object := layer.Objects[index]
all := ObjectCapabilities{Update: true, Paint: object.Type == "TextObject" || object.Type == "PathObject", ReplaceFont: object.Type == "TextObject", Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true}
all.ResetCrop = object.Type == "ImageObject" && object.state.crop != nil
origin := e.objectOrigin(id)
if origin == nil {
all.ReplaceImage = object.Type == "ImageObject"
@@ -426,6 +429,7 @@ func editorPreservedObject(before, after GraphicObject) bool {
return false
}
before = cloneEditorData(before)
before.state = after.state
switch before.Type {
case "TextObject":
a, b := &before.TextObject, after.TextObject
+2
View File
@@ -313,6 +313,7 @@ func (e *Editor) CropImage(page int, id string, box Box) error {
if err := cropImageObject(image, box); err != nil {
return err
}
object.state.crop = nil
return e.UpdateObject(page, id, object)
}
@@ -370,6 +371,7 @@ func (e *Editor) FitImage(page int, id, mode string) error {
if err := e.fitImage(&object.ImageObject, mode); err != nil {
return err
}
object.state.crop = nil
return e.UpdateObject(page, id, object)
}
+1
View File
@@ -59,6 +59,7 @@ type GraphicObject struct {
ImageObject ImageObject
CompositeGraphicUnit CompositeGraphicUnit
origin *editorObjectOrigin
state editorCompositeState
}
// Clips 裁剪区域集合
+28 -2
View File
@@ -211,11 +211,37 @@ func sameShapePath(a, b []string) bool {
// 入参: box 新几何范围,直线使用起点和有符号的端点位移
// 返回: PathObject 调整后的对象, error 错误信息
func (p PathObject) Reshape(box Box) (PathObject, error) {
kind, previous := p.Shape()
kind, _ := p.Shape()
return p.reshape(kind, box)
}
// ReshapeLine 调整直线端点及箭头类型,保留绘制样式、变换及裁剪
// 入参: kind 为ShapeLine、ShapeArrow或ShapeDoubleArrow, box 起点及有符号端点位移
// 返回: PathObject 调整后的直线, error 错误信息
func (p PathObject) ReshapeLine(kind ShapeKind, box Box) (PathObject, error) {
before, _ := p.Shape()
if !lineShapeKind(before) || !lineShapeKind(kind) {
return PathObject{}, fmt.Errorf("path is not a supported line")
}
return p.reshape(kind, box)
}
// lineShapeKind 判断基本图形是否使用直线端点
// 入参: kind 图形类型
// 返回: bool 是否为直线或箭头
func lineShapeKind(kind ShapeKind) bool {
return kind == ShapeLine || kind == ShapeArrow || kind == ShapeDoubleArrow
}
// reshape 在原坐标变换下重建已识别基本图形的几何,不改写样式
// 入参: kind 目标类型, box 新几何范围
// 返回: PathObject 调整后的对象, error 错误信息
func (p PathObject) reshape(kind ShapeKind, box Box) (PathObject, error) {
before, previous := p.Shape()
if kind == "" {
return PathObject{}, fmt.Errorf("path is not a supported basic shape")
}
if box == previous {
if box == previous && kind == before {
return p, nil
}
if !finite(box.X) || !finite(box.Y) || !finite(box.X+box.W) || !finite(box.Y+box.H) {