mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
47562c4e7a
commit
2ddde73b65
16 files changed
+363
-64
No files matched your search
@@ -63,11 +63,11 @@
|
|||||||
<span>文件</span>
|
<span>文件</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group editor-objects">
|
<div class="editor-group editor-objects">
|
||||||
|
<button id="compositeBackButton" class="button" type="button" title="返回上层" hidden>返回</button>
|
||||||
<button id="selectObjectButton" class="button" type="button" aria-pressed="true">选择</button>
|
<button id="selectObjectButton" class="button" type="button" aria-pressed="true">选择</button>
|
||||||
<button id="multiSelectButton" class="button" type="button" aria-pressed="false" title="多选对象">多选</button>
|
<button id="multiSelectButton" class="button" type="button" aria-pressed="false" title="多选对象">多选</button>
|
||||||
<button id="editObjectButton" class="button" type="button" aria-label="修改对象" disabled>修改</button>
|
<button id="editObjectButton" class="button" type="button" aria-label="修改对象" disabled>修改</button>
|
||||||
<button id="copyObjectButton" class="button" type="button" aria-label="复制对象" disabled>复制</button>
|
<button id="copyObjectButton" class="button" type="button" aria-label="复制对象" disabled>复制</button>
|
||||||
<button id="compositeBackButton" class="button" type="button" title="返回上层" hidden>返回</button>
|
|
||||||
<button id="deleteObjectButton" class="button" type="button" disabled>删除</button>
|
<button id="deleteObjectButton" class="button" type="button" disabled>删除</button>
|
||||||
<select id="objectOrder" class="order-select" aria-label="对象层级" disabled>
|
<select id="objectOrder" class="order-select" aria-label="对象层级" disabled>
|
||||||
<option value="" hidden>层级</option>
|
<option value="" hidden>层级</option>
|
||||||
|
|||||||
+18
-10
@@ -1005,7 +1005,7 @@ function openObjectStyle() {
|
|||||||
const alpha = shared("alpha", 255), dash = shared("dashPattern", "");
|
const alpha = shared("alpha", 255), dash = shared("dashPattern", "");
|
||||||
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
||||||
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
||||||
el.objectStrokeFields.hidden = Boolean(state.composite) || !items.every(item => item.type === "PathObject");
|
el.objectStrokeFields.hidden = !items.every(item => item.type === "PathObject" && (!item.scoped || canEditObject(item, "paint")));
|
||||||
el.objectDash.value = dash === null ? "mixed" : dash === "" ? "solid" : "custom";
|
el.objectDash.value = dash === null ? "mixed" : dash === "" ? "solid" : "custom";
|
||||||
el.objectDashPattern.value = dash || "";
|
el.objectDashPattern.value = dash || "";
|
||||||
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
||||||
@@ -1313,7 +1313,7 @@ async function editCanvasObject(item) {
|
|||||||
if (canEditObject(item, "enter")) return enterCompositeScope(item.index, item.id);
|
if (canEditObject(item, "enter")) return enterCompositeScope(item.index, item.id);
|
||||||
if (item.type === "TextObject") {
|
if (item.type === "TextObject") {
|
||||||
if (!confirmTextReflow(item)) return;
|
if (!confirmTextReflow(item)) return;
|
||||||
} else if (!canEditObject(item, "update")) return;
|
} else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
|
||||||
if (item.type === "PathObject") {
|
if (item.type === "PathObject") {
|
||||||
el.shapeWidth.focus();
|
el.shapeWidth.focus();
|
||||||
return;
|
return;
|
||||||
@@ -1425,7 +1425,7 @@ function openInsertPanel(item = null) {
|
|||||||
}
|
}
|
||||||
el.insertForm.reset();
|
el.insertForm.reset();
|
||||||
state.insertObject = item;
|
state.insertObject = item;
|
||||||
el.insertFitRow.hidden = !item;
|
el.insertFitRow.hidden = !item || Boolean(item.scoped);
|
||||||
el.insertPanel.setAttribute("aria-label", item ? "替换图片" : "添加图片");
|
el.insertPanel.setAttribute("aria-label", item ? "替换图片" : "添加图片");
|
||||||
el.insertSubmit.textContent = item ? "替换" : "添加";
|
el.insertSubmit.textContent = item ? "替换" : "添加";
|
||||||
el.insertStatus.textContent = "";
|
el.insertStatus.textContent = "";
|
||||||
@@ -1614,8 +1614,9 @@ async function insertObject(event) {
|
|||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const doc = item
|
const doc = item?.scoped
|
||||||
? await callWASM("ofdgoReplaceImage", item.index, item.id, data, el.insertFit.value)
|
? await callWASM("ofdgoChangeCompositeObjects", item.index, state.composite.key, [item.id], "image", data)
|
||||||
|
: item ? await callWASM("ofdgoReplaceImage", item.index, item.id, data, el.insertFit.value)
|
||||||
: await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2);
|
: await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
@@ -1831,7 +1832,8 @@ async function changeDocument(name, item, ...args) {
|
|||||||
}
|
}
|
||||||
const scope = state.composite;
|
const scope = state.composite;
|
||||||
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
|
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
|
||||||
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor" }[name];
|
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
|
||||||
|
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order" }[name];
|
||||||
const members = item?.items || (item ? [item] : []);
|
const members = item?.items || (item ? [item] : []);
|
||||||
const scoped = scope && members.length && members.every(member => member.scoped);
|
const scoped = scope && members.length && members.every(member => member.scoped);
|
||||||
if (!item) resetCompositeScope();
|
if (!item) resetCompositeScope();
|
||||||
@@ -1872,6 +1874,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
setPan(false);
|
setPan(false);
|
||||||
canvasEditor.pendingSelection = { index: item?.index ?? args[0], ids: doc.selectedIDs };
|
canvasEditor.pendingSelection = { index: item?.index ?? args[0], ids: doc.selectedIDs };
|
||||||
}
|
}
|
||||||
|
if (scoped && name === "ofdgoOrderObjects") canvasEditor.pendingSelection = { index: item.index, ids: doc.selectedIDs };
|
||||||
openSeq = ++state.openSeq;
|
openSeq = ++state.openSeq;
|
||||||
if (item || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
if (item || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||||
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
||||||
@@ -3694,6 +3697,11 @@ function copyEditorSelection(event) {
|
|||||||
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false;
|
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false;
|
||||||
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
||||||
const cut = event.type === "cut";
|
const cut = event.type === "cut";
|
||||||
|
if (state.composite) {
|
||||||
|
event.preventDefault();
|
||||||
|
setStatus("内部对象请用复制按钮");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
|
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setStatus(cut ? "对象不可剪切" : "对象不可复制");
|
setStatus(cut ? "对象不可剪切" : "对象不可复制");
|
||||||
@@ -5536,10 +5544,11 @@ function updateEditorTools() {
|
|||||||
function updateObjectControls(item, reset = false) {
|
function updateObjectControls(item, reset = false) {
|
||||||
el.compositeBackButton.hidden = !state.composite;
|
el.compositeBackButton.hidden = !state.composite;
|
||||||
el.compositeBackButton.disabled = !state.ready || state.exporting;
|
el.compositeBackButton.disabled = !state.ready || state.exporting;
|
||||||
el.deleteObjectButton.hidden = Boolean(state.composite);
|
el.selectObjectButton.hidden = Boolean(state.composite);
|
||||||
|
const members = item?.items || (item ? [item] : []);
|
||||||
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
|
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
|
||||||
el.deleteObjectButton.disabled = disabled || !canEditObject(item, "delete");
|
el.deleteObjectButton.disabled = disabled || !canEditObject(item, "delete");
|
||||||
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy");
|
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy") || Boolean(state.composite && members.some(member => member.container !== members[0].container));
|
||||||
el.objectStyleButton.disabled = disabled || !canEditObject(item, "transform");
|
el.objectStyleButton.disabled = disabled || !canEditObject(item, "transform");
|
||||||
const cropping = Boolean(canvasEditor.crop);
|
const cropping = Boolean(canvasEditor.crop);
|
||||||
el.objectBoundsButton.disabled = disabled || cropping || !canEditObject(item, "transform");
|
el.objectBoundsButton.disabled = disabled || cropping || !canEditObject(item, "transform");
|
||||||
@@ -5555,7 +5564,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.resetCropButton.disabled = el.cropImageButton.disabled || !cropping && (!item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
el.resetCropButton.disabled = el.cropImageButton.disabled || !cropping && (!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.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
||||||
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
||||||
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : "update"));
|
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||||
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
||||||
const selection = selectedText(item);
|
const selection = selectedText(item);
|
||||||
const text = selection ? currentTextStyle() : state.textDefaults;
|
const text = selection ? currentTextStyle() : state.textDefaults;
|
||||||
@@ -5608,7 +5617,6 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.shapeWidth.value = "1";
|
el.shapeWidth.value = "1";
|
||||||
}
|
}
|
||||||
updateDrawingControls();
|
updateDrawingControls();
|
||||||
const members = item?.items || (item ? [item] : []);
|
|
||||||
const orders = members.map(member => member.position).sort((a, b) => a - b);
|
const orders = members.map(member => member.position).sort((a, b) => a - b);
|
||||||
const count = members[0]?.count || 0;
|
const count = members[0]?.count || 0;
|
||||||
const canRaise = orders.some((order, i) => order !== count - orders.length + i);
|
const canRaise = orders.some((order, i) => order !== count - orders.length + i);
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ export function objectEditReason(item) {
|
|||||||
if (!reason) return "";
|
if (!reason) return "";
|
||||||
const message = labels[object.capabilities.reasonCode] || "暂不支持此对象特性";
|
const message = labels[object.capabilities.reasonCode] || "暂不支持此对象特性";
|
||||||
const available = object.capabilities.transform ? object.capabilities.paint ? "仍可移动、改色" : "仍可移动"
|
const available = object.capabilities.transform ? object.capabilities.paint ? "仍可移动、改色" : "仍可移动"
|
||||||
: object.capabilities.copy && object.capabilities.delete ? "仍可复制、删除" : "暂不可编辑";
|
: object.capabilities.replaceImage ? "仍可替换图片" : object.capabilities.copy && object.capabilities.delete ? "仍可复制、删除"
|
||||||
|
: object.capabilities.delete ? "仍可删除" : "暂不可编辑";
|
||||||
return object.capabilities.missingGlyphs ? missingGlyphMessage(object.capabilities.missingGlyphs, available) : `${message},${available}`;
|
return object.capabilities.missingGlyphs ? missingGlyphMessage(object.capabilities.missingGlyphs, available) : `${message},${available}`;
|
||||||
});
|
});
|
||||||
return [...new Set(reasons.filter(Boolean))].join(";");
|
return [...new Set(reasons.filter(Boolean))].join(";");
|
||||||
|
|||||||
+33
-4
@@ -832,7 +832,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
// 返回: map[string]any 前端能力
|
// 返回: map[string]any 前端能力
|
||||||
func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) 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),
|
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),
|
||||||
"enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
"replaceImage": capability.ReplaceImage, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||||
if missing := capability.MissingGlyphs; missing != nil {
|
if missing := capability.MissingGlyphs; missing != nil {
|
||||||
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
||||||
}
|
}
|
||||||
@@ -919,7 +919,7 @@ func compositeObjects(args []js.Value) (any, error) {
|
|||||||
if box.W <= 0 || box.H <= 0 {
|
if box.W <= 0 || box.H <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
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": i, "count": len(members), "container": key, "scoped": true, "contours": member.Contours,
|
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)}
|
"capabilities": editorCapabilities(member.Capabilities, member.Object.Type)}
|
||||||
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
||||||
if member.Object.Type == "TextObject" {
|
if member.Object.Type == "TextObject" {
|
||||||
@@ -932,11 +932,12 @@ func compositeObjects(args []js.Value) (any, error) {
|
|||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeCompositeObjects 复用库层内部选区变换与单次撤销
|
// changeCompositeObjects 提交内部选区操作,结构变化后返回新的选区路径
|
||||||
// 入参: args 页面索引、父路径、成员路径列表、操作及参数
|
// 入参: args 页面索引、父路径、成员路径列表、操作及参数
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func changeCompositeObjects(args []js.Value) (any, error) {
|
func changeCompositeObjects(args []js.Value) (any, error) {
|
||||||
return changeObjects(func() error {
|
var selected []int
|
||||||
|
result, err := changeObjects(func() error {
|
||||||
key := args[1].String()
|
key := args[1].String()
|
||||||
path, err := compositePath(key)
|
path, err := compositePath(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -957,6 +958,23 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
page := args[0].Int()
|
page := args[0].Int()
|
||||||
operation := args[3].String()
|
operation := args[3].String()
|
||||||
switch operation {
|
switch operation {
|
||||||
|
case "delete":
|
||||||
|
return currentEditor.DeleteCompositeObjects(page, path, indexes)
|
||||||
|
case "copy":
|
||||||
|
selected, err = currentEditor.CopyCompositeObjects(page, path, indexes, args[4].Float(), args[5].Float())
|
||||||
|
return err
|
||||||
|
case "order":
|
||||||
|
selected, err = currentEditor.OrderCompositeObjects(page, path, indexes, args[4].String())
|
||||||
|
return err
|
||||||
|
case "image":
|
||||||
|
if len(indexes) != 1 {
|
||||||
|
return fmt.Errorf("image replacement requires one member")
|
||||||
|
}
|
||||||
|
data, err := bytesFromJS(args[4])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return currentEditor.ReplaceCompositeImage(page, path, indexes[0], data)
|
||||||
case "transform":
|
case "transform":
|
||||||
return currentEditor.TransformCompositeObjects(page, path, indexes, args[4].Float(), args[5].Float(), args[6].Float())
|
return currentEditor.TransformCompositeObjects(page, path, indexes, args[4].Float(), args[5].Float(), args[6].Float())
|
||||||
case "rotate":
|
case "rotate":
|
||||||
@@ -1007,6 +1025,17 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
return fmt.Errorf("unsupported composite operation")
|
return fmt.Errorf("unsupported composite operation")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
doc := editorSelectionInfo{editorInfo: result.(editorInfo)}
|
||||||
|
for _, index := range selected {
|
||||||
|
doc.SelectedIDs = append(doc.SelectedIDs, fmt.Sprintf("%s/%d", args[1].String(), index))
|
||||||
|
}
|
||||||
|
return doc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorFont 读取对象实际使用的内嵌字体,供画布输入使用
|
// editorFont 读取对象实际使用的内嵌字体,供画布输入使用
|
||||||
|
|||||||
+13
-7
@@ -219,6 +219,17 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
|||||||
for _, index := range indexes {
|
for _, index := range indexes {
|
||||||
selected[slices.Index(siblings, index.index)] = true
|
selected[slices.Index(siblings, index.index)] = true
|
||||||
}
|
}
|
||||||
|
orderEditorObjects(objects, selected, order)
|
||||||
|
for i, index := range siblings {
|
||||||
|
layers[indexes[0].layer].Objects[index] = objects[i]
|
||||||
|
}
|
||||||
|
e.replaceLayers(page, layers)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// orderEditorObjects 调整选区层级,保持选中及未选中对象各自的相对顺序
|
||||||
|
// 入参: objects 同容器对象, selected 选中状态, order 排序方向
|
||||||
|
func orderEditorObjects[T any](objects []T, selected []bool, order string) {
|
||||||
switch order {
|
switch order {
|
||||||
case "up":
|
case "up":
|
||||||
for i := len(objects) - 2; i >= 0; i-- {
|
for i := len(objects) - 2; i >= 0; i-- {
|
||||||
@@ -235,7 +246,7 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
ordered := make([]GraphicObject, 0, len(objects))
|
ordered := make([]T, 0, len(objects))
|
||||||
for _, takeSelected := range []bool{order == "bottom", order != "bottom"} {
|
for _, takeSelected := range []bool{order == "bottom", order != "bottom"} {
|
||||||
for i, object := range objects {
|
for i, object := range objects {
|
||||||
if selected[i] == takeSelected {
|
if selected[i] == takeSelected {
|
||||||
@@ -243,13 +254,8 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
objects = ordered
|
copy(objects, ordered)
|
||||||
}
|
}
|
||||||
for i, index := range siblings {
|
|
||||||
layers[indexes[0].layer].Objects[index] = objects[i]
|
|
||||||
}
|
|
||||||
e.replaceLayers(page, layers)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序
|
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序
|
||||||
|
|||||||
+40
-34
@@ -34,12 +34,14 @@ type ObjectPath struct {
|
|||||||
|
|
||||||
// CompositeMember 复合对象的直接成员快照,Object包含有效样式,不用于整体替换原对象
|
// CompositeMember 复合对象的直接成员快照,Object包含有效样式,不用于整体替换原对象
|
||||||
// Bounds与Contours使用页面毫米坐标,StrokeScale为对象描边到页面的倍率
|
// Bounds与Contours使用页面毫米坐标,StrokeScale为对象描边到页面的倍率
|
||||||
|
// Position表示当前范围内的直接容器位置,结构操作后需重新枚举
|
||||||
type CompositeMember struct {
|
type CompositeMember struct {
|
||||||
Object GraphicObject
|
Object GraphicObject
|
||||||
Bounds Box
|
Bounds Box
|
||||||
Contours []ObjectContour
|
Contours []ObjectContour
|
||||||
StrokeScale float64
|
StrokeScale float64
|
||||||
Capabilities ObjectCapabilities
|
Capabilities ObjectCapabilities
|
||||||
|
Position ObjectPosition
|
||||||
Transform bool
|
Transform bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,9 @@ type editorCompositeNode struct {
|
|||||||
expanded bool
|
expanded bool
|
||||||
changed bool
|
changed bool
|
||||||
span *editorXML
|
span *editorXML
|
||||||
|
owner *editorCompositeNode
|
||||||
|
patches []editorXMLPatch
|
||||||
|
index int
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompositeObjects 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级
|
// CompositeObjects 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级
|
||||||
@@ -189,6 +194,9 @@ func (e *Editor) compositeScope(page int, path ObjectPath) (*Reader, *Renderer,
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if depth == len(path.Children) {
|
if depth == len(path.Children) {
|
||||||
|
for i, member := range members {
|
||||||
|
member.index = i
|
||||||
|
}
|
||||||
return reader, renderer, root, members, nil
|
return reader, renderer, root, members, nil
|
||||||
}
|
}
|
||||||
i := path.Children[depth]
|
i := path.Children[depth]
|
||||||
@@ -300,6 +308,7 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
}
|
}
|
||||||
item.parent, item.boundaryInCTM, item.defaults, item.clip, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, visible, child
|
item.parent, item.boundaryInCTM, item.defaults, item.clip, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, visible, child
|
||||||
item.drawParams = drawParams
|
item.drawParams = drawParams
|
||||||
|
item.owner = n
|
||||||
n.children = append(n.children, item)
|
n.children = append(n.children, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,6 +341,18 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
// 返回: []CompositeMember 独立快照及操作能力
|
// 返回: []CompositeMember 独立快照及操作能力
|
||||||
func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorCompositeNode) []CompositeMember {
|
func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorCompositeNode) []CompositeMember {
|
||||||
result := make([]CompositeMember, len(nodes))
|
result := make([]CompositeMember, len(nodes))
|
||||||
|
positions := make(map[*editorXML]ObjectPosition)
|
||||||
|
orderable := make(map[*editorXML]bool)
|
||||||
|
for _, node := range nodes {
|
||||||
|
parent := node.span.parent
|
||||||
|
position := positions[parent]
|
||||||
|
if position.Count == 0 {
|
||||||
|
position.Container = fmt.Sprint(node.index)
|
||||||
|
orderable[parent] = editorContainerOrderable(parent)
|
||||||
|
}
|
||||||
|
position.Count++
|
||||||
|
positions[parent] = position
|
||||||
|
}
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
bounds := &boundsRenderer{collect: true}
|
bounds := &boundsRenderer{collect: true}
|
||||||
object := node.object
|
object := node.object
|
||||||
@@ -346,7 +367,8 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
|||||||
_, invertible := node.parent.Invert()
|
_, invertible := node.parent.Invert()
|
||||||
borderActions := node.object.Type == "ImageObject" && node.object.ImageObject.Border != nil && len(node.object.ImageObject.Actions) != 0
|
borderActions := node.object.Type == "ImageObject" && node.object.ImageObject.Border != nil && len(node.object.ImageObject.Actions) != 0
|
||||||
transform := invertible && editorXMLTransformable(node.node) && validateEditorGeometry(node.object) == nil && !borderActions
|
transform := invertible && editorXMLTransformable(node.node) && validateEditorGeometry(node.object) == nil && !borderActions
|
||||||
capability := ObjectCapabilities{Transform: transform, Arrange: transform && bounds.box.W > 0 && bounds.box.H > 0}
|
capability := ObjectCapabilities{Transform: transform, Arrange: transform && bounds.box.W > 0 && bounds.box.H > 0,
|
||||||
|
Delete: true, Copy: transform && editorXMLCopyable(node.node), Order: orderable[node.span.parent], ReplaceImage: node.object.Type == "ImageObject"}
|
||||||
style, err := e.compositeMemberStyle(node)
|
style, err := e.compositeMemberStyle(node)
|
||||||
if !transform {
|
if !transform {
|
||||||
capability.ReasonCode, capability.Reason = EditUnsupportedContainer, "composite member cannot be transformed"
|
capability.ReasonCode, capability.Reason = EditUnsupportedContainer, "composite member cannot be transformed"
|
||||||
@@ -360,7 +382,10 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
|||||||
}
|
}
|
||||||
_, ctm := editorGeometry(node.object)
|
_, ctm := editorGeometry(node.object)
|
||||||
matrix := node.parent.Multiply(NewMatrix(ctm))
|
matrix := node.parent.Multiply(NewMatrix(ctm))
|
||||||
result[i] = CompositeMember{Object: style, Bounds: bounds.box, Contours: bounds.contours, StrokeScale: math.Sqrt(math.Abs(matrix.a*matrix.d - matrix.b*matrix.c)), Capabilities: capability, Transform: transform}
|
position := positions[node.span.parent]
|
||||||
|
result[i] = CompositeMember{Object: style, Bounds: bounds.box, Contours: bounds.contours, StrokeScale: math.Sqrt(math.Abs(matrix.a*matrix.d - matrix.b*matrix.c)), Capabilities: capability, Position: position, Transform: transform}
|
||||||
|
position.Index++
|
||||||
|
positions[node.span.parent] = position
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -386,15 +411,12 @@ func (e *Editor) compositeMemberStyle(node *editorCompositeNode) (GraphicObject,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽
|
// StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽
|
||||||
// 不重排文字,不改写继承参数;暂不支持虚线及端点样式
|
// 不重排文字,不改写继承参数;显式实线覆盖原虚线继承
|
||||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) error {
|
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) error {
|
||||||
if style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil {
|
|
||||||
return fmt.Errorf("composite stroke parameters are not supported")
|
|
||||||
}
|
|
||||||
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil
|
paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil || style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
capability := members[i].Capabilities
|
capability := members[i].Capabilities
|
||||||
if !capability.Transform || paint && !capability.Paint {
|
if !capability.Transform || paint && !capability.Paint {
|
||||||
@@ -417,18 +439,9 @@ func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data, err := editorXMLObject(node.data, node.node, node.object, object)
|
if err := node.update(object); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if bytes.Equal(data, node.data) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
updated, err := newEditorCompositeNode(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
node.data, node.node, node.object, node.changed = data, updated.node, updated.object, true
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -548,6 +561,14 @@ func (e *Editor) editCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
ready := e.source.idsReady
|
ready := e.source.idsReady
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
removed := make(map[string]bool)
|
||||||
|
for _, resource := range e.resources[count:] {
|
||||||
|
if resource.image != nil {
|
||||||
|
removed[resource.image.ID] = true
|
||||||
|
delete(e.images, resource.image.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maps.DeleteFunc(e.resourceID, func(_ editorResourceKey, id string) bool { return removed[id] })
|
||||||
clear(e.resources[count:])
|
clear(e.resources[count:])
|
||||||
e.resources, e.maxID = e.resources[:count], maximum
|
e.resources, e.maxID = e.resources[:count], maximum
|
||||||
e.source.idsReady = ready
|
e.source.idsReady = ready
|
||||||
@@ -698,7 +719,7 @@ func compositeBoundary(object GraphicObject, parent Matrix, inward bool) Graphic
|
|||||||
// 入参: node 实例节点
|
// 入参: node 实例节点
|
||||||
// 返回: []byte 新片段, bool 是否变化, error 错误信息
|
// 返回: []byte 新片段, bool 是否变化, error 错误信息
|
||||||
func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, error) {
|
func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, error) {
|
||||||
var patches []editorXMLPatch
|
patches := slices.Clone(node.patches)
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
data, changed, err := e.writeCompositeNode(child)
|
data, changed, err := e.writeCompositeNode(child)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -748,22 +769,7 @@ func (e *Editor) addCompositeResource(data []byte) (string, error) {
|
|||||||
return "", fmt.Errorf("composite contains unsupported identifiers")
|
return "", fmt.Errorf("composite contains unsupported identifiers")
|
||||||
}
|
}
|
||||||
ids := make(map[string]string)
|
ids := make(map[string]string)
|
||||||
var collect func(*editorXML) error
|
if err := e.collectCompositeIDs(root, ids); err != nil {
|
||||||
collect = func(node *editorXML) error {
|
|
||||||
if id := editorResourceID(node.attr("ID")); id != "" {
|
|
||||||
if ids[id] != "" {
|
|
||||||
return fmt.Errorf("duplicate object ID %q", id)
|
|
||||||
}
|
|
||||||
ids[id] = e.nextID()
|
|
||||||
}
|
|
||||||
for _, child := range node.children {
|
|
||||||
if err := collect(child); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := collect(root); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
id := ids[editorResourceID(root.attr("ID"))]
|
id := ids[editorResourceID(root.attr("ID"))]
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// siblings 获取同一原始容器内的直接绘制成员,不跨越资源和页块
|
||||||
|
// 返回: []*editorCompositeNode 按绘制顺序排列的成员
|
||||||
|
func (n *editorCompositeNode) siblings() []*editorCompositeNode {
|
||||||
|
var result []*editorCompositeNode
|
||||||
|
for _, member := range n.owner.children {
|
||||||
|
if member.span.parent == n.span.parent {
|
||||||
|
result = append(result, member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCompositeObjects 删除内部成员,保留容器、未选对象及其他实例,一次撤销恢复全部
|
||||||
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) DeleteCompositeObjects(page int, path ObjectPath, indexes []int) error {
|
||||||
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error {
|
||||||
|
for _, node := range nodes {
|
||||||
|
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, nil})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyCompositeObjects 在原容器末尾复制选区并按页面毫米平移,保留原文并重映射副本内部标识
|
||||||
|
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号, dx、dy 页面位移
|
||||||
|
// 返回: []int 按绘制顺序排列的副本序号, error 错误信息
|
||||||
|
func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int, dx, dy float64) ([]int, error) {
|
||||||
|
if !finite(dx) || !finite(dy) {
|
||||||
|
return nil, fmt.Errorf("copy requires finite offsets")
|
||||||
|
}
|
||||||
|
var result []int
|
||||||
|
err := e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
container := nodes[0].span.parent
|
||||||
|
for i, node := range nodes {
|
||||||
|
if !members[i].Capabilities.Copy || node.span.parent != container {
|
||||||
|
return fmt.Errorf("copy requires editable members in the same container")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
|
||||||
|
ids := make(map[string]string)
|
||||||
|
for _, node := range nodes {
|
||||||
|
if err := e.collectCompositeIDs(node.node, ids); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
for _, node := range nodes {
|
||||||
|
copy := *node
|
||||||
|
if err := e.transformCompositeMember(renderer, ©, TranslationMatrix(dx, dy)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = append(data, fragment...)
|
||||||
|
}
|
||||||
|
owner := nodes[0].owner
|
||||||
|
owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, data})
|
||||||
|
next := 0
|
||||||
|
for _, member := range owner.children {
|
||||||
|
if container.start < member.span.start && member.span.end <= container.close {
|
||||||
|
next = member.index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range nodes {
|
||||||
|
result = append(result, next+i)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例
|
||||||
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, order 为up、down、top或bottom
|
||||||
|
// 返回: []int 调整后的选区序号, error 错误信息
|
||||||
|
func (e *Editor) OrderCompositeObjects(page int, path ObjectPath, indexes []int, order string) ([]int, error) {
|
||||||
|
if !slices.Contains([]string{"up", "down", "top", "bottom"}, order) {
|
||||||
|
return nil, fmt.Errorf("invalid object order %q", order)
|
||||||
|
}
|
||||||
|
var result []int
|
||||||
|
err := e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
siblings := nodes[0].siblings()
|
||||||
|
selected := make([]bool, len(siblings))
|
||||||
|
for j, node := range nodes {
|
||||||
|
i := slices.Index(siblings, node)
|
||||||
|
if i < 0 || !members[j].Capabilities.Order {
|
||||||
|
return fmt.Errorf("ordering requires members in the same container")
|
||||||
|
}
|
||||||
|
selected[i] = true
|
||||||
|
}
|
||||||
|
ordered := slices.Clone(siblings)
|
||||||
|
orderEditorObjects(ordered, selected, order)
|
||||||
|
for i, node := range siblings {
|
||||||
|
if node != ordered[i] {
|
||||||
|
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, ordered[i].data})
|
||||||
|
}
|
||||||
|
if slices.Contains(nodes, ordered[i]) {
|
||||||
|
result = append(result, node.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceCompositeImage 替换内部图片数据,保留变换、裁剪、蒙版、边框和透明度
|
||||||
|
// 入参: page 页面索引, path 父复合路径, index 图片序号, data PNG或JPEG数据
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, data []byte) error {
|
||||||
|
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error {
|
||||||
|
node := nodes[0]
|
||||||
|
if node.object.Type != "ImageObject" {
|
||||||
|
return fmt.Errorf("composite member is not an image")
|
||||||
|
}
|
||||||
|
object := node.object
|
||||||
|
id, err := e.AddImage(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object.ImageObject.ResourceID = id
|
||||||
|
return node.update(object)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// update 合并成员属性变化,不重写原文中未修改的字段
|
||||||
|
// 入参: object 修改后的对象
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (n *editorCompositeNode) update(object GraphicObject) error {
|
||||||
|
data, err := editorXMLObject(n.data, n.node, n.object, object)
|
||||||
|
if err != nil || bytes.Equal(data, n.data) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
next, err := newEditorCompositeNode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.data, n.node, n.object, n.changed = data, next.node, next.object, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectCompositeIDs 为已校验副本中的标准标识分配新值,拒绝重复标识
|
||||||
|
// 入参: node 原节点, ids 原标识与新标识
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) collectCompositeIDs(node *editorXML, ids map[string]string) error {
|
||||||
|
if id := editorResourceID(node.attr("ID")); id != "" {
|
||||||
|
if ids[id] != "" {
|
||||||
|
return fmt.Errorf("duplicate object ID %q", id)
|
||||||
|
}
|
||||||
|
ids[id] = e.nextID()
|
||||||
|
}
|
||||||
|
for _, child := range node.children {
|
||||||
|
if err := e.collectCompositeIDs(child, ids); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -62,11 +62,13 @@ type editorSourcePage struct {
|
|||||||
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
||||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
||||||
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
||||||
|
// ReplaceImage表示可替换图片数据,内部成员保留原布局,不开放裁剪或重新适应
|
||||||
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
||||||
type ObjectCapabilities struct {
|
type ObjectCapabilities struct {
|
||||||
Update bool
|
Update bool
|
||||||
Paint bool
|
Paint bool
|
||||||
ReplaceFont bool
|
ReplaceFont bool
|
||||||
|
ReplaceImage bool
|
||||||
Reflow bool
|
Reflow bool
|
||||||
LayoutKnown bool
|
LayoutKnown bool
|
||||||
Transform bool
|
Transform bool
|
||||||
@@ -355,6 +357,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
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 := 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}
|
||||||
origin := e.objectOrigin(id)
|
origin := e.objectOrigin(id)
|
||||||
if origin == nil {
|
if origin == nil {
|
||||||
|
all.ReplaceImage = object.Type == "ImageObject"
|
||||||
return all, nil
|
return all, nil
|
||||||
}
|
}
|
||||||
node := origin.node
|
node := origin.node
|
||||||
@@ -384,6 +387,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
errors.As(err, &all.MissingGlyphs)
|
errors.As(err, &all.MissingGlyphs)
|
||||||
}
|
}
|
||||||
all.Paint = all.Paint && e.editorPaintable(object)
|
all.Paint = all.Paint && e.editorPaintable(object)
|
||||||
|
all.ReplaceImage = all.Update && object.Type == "ImageObject"
|
||||||
if object.Type == "ImageObject" && object.ImageObject.Border != nil && len(object.ImageObject.Actions) != 0 {
|
if object.Type == "ImageObject" && object.ImageObject.Border != nil && len(object.ImageObject.Actions) != 0 {
|
||||||
all.Transform, all.Arrange = false, false
|
all.Transform, all.Arrange = false, false
|
||||||
all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject
|
all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject
|
||||||
@@ -425,6 +429,7 @@ func editorPreservedObject(before, after GraphicObject) bool {
|
|||||||
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
|
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
|
||||||
a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
|
a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
|
||||||
a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join
|
a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join
|
||||||
|
a.dashPatternSet = b.dashPatternSet
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
a, b := &before.ImageObject, after.ImageObject
|
a, b := &before.ImageObject, after.ImageObject
|
||||||
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
|
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
|
||||||
|
|||||||
@@ -90,11 +90,12 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
|
|||||||
if object.Type == "PathObject" {
|
if object.Type == "PathObject" {
|
||||||
obj := &object.PathObject
|
obj := &object.PathObject
|
||||||
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, Join: obj.Join, Cap: obj.Cap,
|
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, Join: obj.Join, Cap: obj.Cap,
|
||||||
MiterLimit: obj.MiterLimit, DashPattern: obj.DashPattern, DashOffset: obj.DashOffset,
|
MiterLimit: obj.MiterLimit, DashPattern: obj.DashPattern, dashPatternSet: obj.dashPatternSet, DashOffset: obj.DashOffset,
|
||||||
FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
|
FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
|
||||||
obj.DrawParam = ""
|
obj.DrawParam = ""
|
||||||
obj.LineWidth, obj.Join, obj.Cap = style.LineWidth, style.Join, style.Cap
|
obj.LineWidth, obj.Join, obj.Cap = style.LineWidth, style.Join, style.Cap
|
||||||
obj.MiterLimit, obj.DashPattern, obj.DashOffset = style.MiterLimit, style.DashPattern, style.DashOffset
|
obj.MiterLimit, obj.DashPattern, obj.DashOffset = style.MiterLimit, style.DashPattern, style.DashOffset
|
||||||
|
obj.dashPatternSet = style.dashPatternSet
|
||||||
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
||||||
} else {
|
} else {
|
||||||
obj := &object.TextObject
|
obj := &object.TextObject
|
||||||
@@ -221,6 +222,7 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
|
|||||||
}
|
}
|
||||||
if style.DashPattern != nil {
|
if style.DashPattern != nil {
|
||||||
path.DashPattern = *style.DashPattern
|
path.DashPattern = *style.DashPattern
|
||||||
|
path.dashPatternSet = path.DashPattern == ""
|
||||||
}
|
}
|
||||||
if style.DashOffset != nil {
|
if style.DashOffset != nil {
|
||||||
path.DashOffset = style.DashOffset
|
path.DashOffset = style.DashOffset
|
||||||
@@ -316,6 +318,7 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) error {
|
|||||||
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor
|
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor
|
||||||
to.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
|
to.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
|
||||||
to.DashPattern, to.DashOffset = from.DashPattern, from.DashOffset
|
to.DashPattern, to.DashOffset = from.DashPattern, from.DashOffset
|
||||||
|
to.dashPatternSet = from.dashPatternSet
|
||||||
scale := editorStrokeScale(from.CTM) / editorStrokeScale(to.CTM)
|
scale := editorStrokeScale(from.CTM) / editorStrokeScale(to.CTM)
|
||||||
if scale != 1 {
|
if scale != 1 {
|
||||||
if to.LineWidth == 0 {
|
if to.LineWidth == 0 {
|
||||||
|
|||||||
+10
-3
@@ -104,6 +104,13 @@ func (n *editorXML) attr(name string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasAttr 判断属性是否存在,区分空值与省略
|
||||||
|
// 入参: name 属性名称
|
||||||
|
// 返回: bool 是否存在
|
||||||
|
func (n *editorXML) hasAttr(name string) bool {
|
||||||
|
return slices.ContainsFunc(n.attrs, func(attr xml.Attr) bool { return attr.Name.Space == "" && attr.Name.Local == name })
|
||||||
|
}
|
||||||
|
|
||||||
// editorPatchXML 按位置应用互不重叠的替换
|
// editorPatchXML 按位置应用互不重叠的替换
|
||||||
// 入参: data 原文, patches 修改区间
|
// 入参: data 原文, patches 修改区间
|
||||||
// 返回: []byte 修改后的XML
|
// 返回: []byte 修改后的XML
|
||||||
@@ -363,7 +370,7 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
|
|||||||
}
|
}
|
||||||
changes := make(map[string]string)
|
changes := make(map[string]string)
|
||||||
for _, attr := range append(oldNode.attrs, newNode.attrs...) {
|
for _, attr := range append(oldNode.attrs, newNode.attrs...) {
|
||||||
if attr.Name.Space == "" && oldNode.attr(attr.Name.Local) != newNode.attr(attr.Name.Local) {
|
if attr.Name.Space == "" && (oldNode.attr(attr.Name.Local) != newNode.attr(attr.Name.Local) || oldNode.hasAttr(attr.Name.Local) != newNode.hasAttr(attr.Name.Local)) {
|
||||||
changes[attr.Name.Local] = newNode.attr(attr.Name.Local)
|
changes[attr.Name.Local] = newNode.attr(attr.Name.Local)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,7 +395,7 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
|
|||||||
if value, ok := changes[attr.Name.Local]; ok {
|
if value, ok := changes[attr.Name.Local]; ok {
|
||||||
attr.Value = value
|
attr.Value = value
|
||||||
delete(changes, attr.Name.Local)
|
delete(changes, attr.Name.Local)
|
||||||
if value == "" {
|
if !newNode.hasAttr(attr.Name.Local) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,7 +409,7 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
|
|||||||
result.WriteByte('"')
|
result.WriteByte('"')
|
||||||
}
|
}
|
||||||
for _, attr := range newNode.attrs {
|
for _, attr := range newNode.attrs {
|
||||||
if value, ok := changes[attr.Name.Local]; ok && value != "" {
|
if value, ok := changes[attr.Name.Local]; ok {
|
||||||
result.WriteString(" " + attr.Name.Local + "=\"")
|
result.WriteString(" " + attr.Name.Local + "=\"")
|
||||||
_ = xml.EscapeText(&result, []byte(value))
|
_ = xml.EscapeText(&result, []byte(value))
|
||||||
result.WriteByte('"')
|
result.WriteByte('"')
|
||||||
|
|||||||
@@ -19,6 +19,46 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UnmarshalXML 解析路径并区分省略的虚线样式与显式实线
|
||||||
|
// 入参: d XML解码器, start 起始节点
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
type plain PathObject
|
||||||
|
var value struct {
|
||||||
|
plain
|
||||||
|
DashPattern *string `xml:"DashPattern,attr"`
|
||||||
|
}
|
||||||
|
if err := d.DecodeElement(&value, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*p = PathObject(value.plain)
|
||||||
|
if value.DashPattern != nil {
|
||||||
|
p.DashPattern = *value.DashPattern
|
||||||
|
p.dashPatternSet = p.DashPattern == ""
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML 解析绘制参数并保留显式实线对基础参数的覆盖
|
||||||
|
// 入参: d XML解码器, start 起始节点
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (p *DrawParam) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
type plain DrawParam
|
||||||
|
var value struct {
|
||||||
|
plain
|
||||||
|
DashPattern *string `xml:"DashPattern,attr"`
|
||||||
|
}
|
||||||
|
if err := d.DecodeElement(&value, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*p = DrawParam(value.plain)
|
||||||
|
if value.DashPattern != nil {
|
||||||
|
p.DashPattern = *value.DashPattern
|
||||||
|
p.dashPatternSet = p.DashPattern == ""
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析填充颜色并区分未支持的复杂颜色
|
// UnmarshalXML 解析填充颜色并区分未支持的复杂颜色
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ type PathObject struct {
|
|||||||
Cap string `xml:"Cap,attr"`
|
Cap string `xml:"Cap,attr"`
|
||||||
DashOffset *float64 `xml:"DashOffset,attr"`
|
DashOffset *float64 `xml:"DashOffset,attr"`
|
||||||
DashPattern string `xml:"DashPattern,attr"`
|
DashPattern string `xml:"DashPattern,attr"`
|
||||||
|
dashPatternSet bool
|
||||||
MiterLimit float64 `xml:"MiterLimit,attr"`
|
MiterLimit float64 `xml:"MiterLimit,attr"`
|
||||||
CTM string `xml:"CTM,attr"`
|
CTM string `xml:"CTM,attr"`
|
||||||
Alpha *int `xml:"Alpha,attr"`
|
Alpha *int `xml:"Alpha,attr"`
|
||||||
|
|||||||
@@ -249,8 +249,9 @@ func mergeDrawParam(base DrawParam, dp *DrawParam) *DrawParam {
|
|||||||
if dp.Cap != "" {
|
if dp.Cap != "" {
|
||||||
base.Cap = dp.Cap
|
base.Cap = dp.Cap
|
||||||
}
|
}
|
||||||
if dp.DashPattern != "" {
|
if dp.DashPattern != "" || dp.dashPatternSet {
|
||||||
base.DashPattern = dp.DashPattern
|
base.DashPattern = dp.DashPattern
|
||||||
|
base.dashPatternSet = dp.dashPatternSet
|
||||||
}
|
}
|
||||||
if dp.DashOffset != nil {
|
if dp.DashOffset != nil {
|
||||||
base.DashOffset = dp.DashOffset
|
base.DashOffset = dp.DashOffset
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func (s *pathStyle) applyDrawParam(r *Renderer, dp *DrawParam, bx, by, pageH flo
|
|||||||
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
|
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
|
||||||
}
|
}
|
||||||
s.applyLineJoin(dp.Join, dp.MiterLimit)
|
s.applyLineJoin(dp.Join, dp.MiterLimit)
|
||||||
if dp.DashPattern != "" {
|
if dp.DashPattern != "" || dp.dashPatternSet {
|
||||||
s.dashPattern = parseFloats(dp.DashPattern)
|
s.dashPattern = parseFloats(dp.DashPattern)
|
||||||
}
|
}
|
||||||
if dp.DashOffset != nil {
|
if dp.DashOffset != nil {
|
||||||
@@ -150,7 +150,7 @@ func (s *pathStyle) applyPathObject(r *Renderer, obj PathObject, bx, by, pageH f
|
|||||||
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
|
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
|
||||||
}
|
}
|
||||||
s.applyLineJoin(obj.Join, obj.MiterLimit)
|
s.applyLineJoin(obj.Join, obj.MiterLimit)
|
||||||
if obj.DashPattern != "" {
|
if obj.DashPattern != "" || obj.dashPatternSet {
|
||||||
s.dashPattern = parseFloats(obj.DashPattern)
|
s.dashPattern = parseFloats(obj.DashPattern)
|
||||||
}
|
}
|
||||||
if obj.DashOffset != nil {
|
if obj.DashOffset != nil {
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ type DrawParam struct {
|
|||||||
Cap string `xml:"Cap,attr"`
|
Cap string `xml:"Cap,attr"`
|
||||||
DashOffset *float64 `xml:"DashOffset,attr"`
|
DashOffset *float64 `xml:"DashOffset,attr"`
|
||||||
DashPattern string `xml:"DashPattern,attr"`
|
DashPattern string `xml:"DashPattern,attr"`
|
||||||
|
dashPatternSet bool
|
||||||
MiterLimit float64 `xml:"MiterLimit,attr"`
|
MiterLimit float64 `xml:"MiterLimit,attr"`
|
||||||
Font string `xml:"Font,attr"`
|
Font string `xml:"Font,attr"`
|
||||||
Size float64 `xml:"Size,attr"`
|
Size float64 `xml:"Size,attr"`
|
||||||
|
|||||||
@@ -637,7 +637,11 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
|||||||
attrs.add("Join", obj.Join)
|
attrs.add("Join", obj.Join)
|
||||||
attrs.add("Cap", obj.Cap)
|
attrs.add("Cap", obj.Cap)
|
||||||
attrs.add("Rule", obj.Rule)
|
attrs.add("Rule", obj.Rule)
|
||||||
|
if obj.dashPatternSet {
|
||||||
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: obj.DashPattern})
|
||||||
|
} else {
|
||||||
attrs.add("DashPattern", obj.DashPattern)
|
attrs.add("DashPattern", obj.DashPattern)
|
||||||
|
}
|
||||||
if obj.DashOffset != nil {
|
if obj.DashOffset != nil {
|
||||||
attrs.add("DashOffset", ofdNumber(*obj.DashOffset))
|
attrs.add("DashOffset", ofdNumber(*obj.DashOffset))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user