mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
0f86c8d727
commit
f8eeaad9f2
16 files changed
+546
-109
No files matched your search
@@ -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
@@ -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"));
|
||||
|
||||
@@ -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();
|
||||
|
||||
+60
-7
@@ -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,15 +2295,22 @@ 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())
|
||||
for i := range points {
|
||||
point := args[2].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 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 := value.Index(i)
|
||||
points[i] = ofdgo.Point{X: point.Get("x").Float(), Y: point.Get("y").Float()}
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
// eraseObjects 按页面矩形范围擦除对象,保留局部裁剪之外的内容
|
||||
// 入参: args 页面索引、对象标识数组和毫米坐标范围
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
|
||||
+4
-4
@@ -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)
|
||||
}
|
||||
|
||||
+22
-10
@@ -72,17 +72,17 @@ 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
|
||||
exists bool
|
||||
indexes []int
|
||||
exists bool
|
||||
}
|
||||
|
||||
// setStates 独立保存当前XML子树的会话信息,共享不可变记录
|
||||
@@ -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
@@ -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")
|
||||
return 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")
|
||||
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(©.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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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 && 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)
|
||||
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++
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 错误信息
|
||||
|
||||
@@ -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
@@ -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 按矩形或闭合路径裁剪对象,只在全部对象处理成功后提交修改
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ type GraphicObject struct {
|
||||
ImageObject ImageObject
|
||||
CompositeGraphicUnit CompositeGraphicUnit
|
||||
origin *editorObjectOrigin
|
||||
state editorCompositeState
|
||||
}
|
||||
|
||||
// Clips 裁剪区域集合
|
||||
|
||||
+28
-2
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user