diff --git a/assets/webui/index.html b/assets/webui/index.html
index dda6a9f..ca2d7b6 100644
--- a/assets/webui/index.html
+++ b/assets/webui/index.html
@@ -58,7 +58,15 @@
+
+
对象
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css
index dcdd4d4..ecb157e 100644
--- a/assets/webui/ofdgo.css
+++ b/assets/webui/ofdgo.css
@@ -104,6 +104,10 @@ a:focus-visible {
border-right: 1px solid var(--line);
}
+.editor-group:has(#selectObjectButton) {
+ grid-template-columns: repeat(5, max-content);
+}
+
.editor-group:has(#insertTextButton) {
grid-template-columns: repeat(2, max-content);
}
@@ -116,12 +120,14 @@ a:focus-visible {
border-right: 0;
}
-.editor-group > button {
+.editor-group > button,
+.editor-group > select {
grid-row: 1;
}
.editor-group > span {
grid-column: 1 / -1;
+ align-self: end;
text-align: center;
font-size: 12px;
color: var(--text-muted);
@@ -314,7 +320,8 @@ button:not(:disabled):hover {
margin: 0;
}
-.format-select {
+.format-select,
+.order-select {
width: 58px;
height: var(--control-height);
min-width: 0;
@@ -424,8 +431,26 @@ button:not(:disabled):hover {
}
.form-row input[type="file"] {
- height: auto;
- padding: 4px;
+ padding: 0;
+ color: var(--text-muted);
+}
+
+.form-row input[type="file"]::file-selector-button {
+ height: 100%;
+ margin: 0 8px 0 0;
+ padding: 0 8px;
+ border: 0;
+ border-right: 1px solid var(--line);
+ border-radius: 0;
+ background: var(--panel);
+ color: #1f2428;
+ font: inherit;
+ cursor: pointer;
+}
+
+.form-row input[type="file"]::file-selector-button:hover {
+ background: #e4f3ef;
+ color: var(--accent-strong);
}
.form-unit {
@@ -475,13 +500,16 @@ button:not(:disabled):hover {
}
.format-select:focus,
-.format-select:focus-visible {
+.format-select:focus-visible,
+.order-select:focus,
+.order-select:focus-visible {
border-color: var(--accent);
outline: 2px solid var(--accent);
outline-offset: 2px;
}
-.format-select option:checked {
+.format-select option:checked,
+.order-select option:checked {
background-color: #e4f3ef;
color: var(--accent-strong);
}
diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js
index 03415cb..8a625e1 100644
--- a/assets/webui/ofdgo.js
+++ b/assets/webui/ofdgo.js
@@ -1,4 +1,4 @@
-import { CanvasEditor } from "./ofdgo_edit.js";
+import { CanvasEditor, objectTransform } from "./ofdgo_edit.js";
import { FontManager } from "./ofdgo_font.js";
const MM_TO_PX = 96 / 25.4;
@@ -87,6 +87,8 @@ const el = {
selectObjectButton: document.querySelector("#selectObjectButton"),
deleteObjectButton: document.querySelector("#deleteObjectButton"),
editTextButton: document.querySelector("#editTextButton"),
+ copyObjectButton: document.querySelector("#copyObjectButton"),
+ objectOrder: document.querySelector("#objectOrder"),
undoButton: document.querySelector("#undoButton"),
redoButton: document.querySelector("#redoButton"),
insertTextButton: document.querySelector("#insertTextButton"),
@@ -247,6 +249,27 @@ el.editTextButton.addEventListener("click", () => {
openInsertPanel(true, canvasEditor.selected);
}
});
+el.copyObjectButton.addEventListener("click", () => {
+ const item = canvasEditor.selected;
+ if (!item) {
+ return;
+ }
+ let change = objectTransform(item, item.page, 3, 3);
+ if (!change.x && !change.y) {
+ change = objectTransform(item, item.page, -3, -3);
+ }
+ return changeDocument("ofdgoCopyObject", item, change.x, change.y);
+});
+el.objectOrder.addEventListener("change", () => {
+ const action = el.objectOrder.value;
+ el.objectOrder.value = "";
+ const item = canvasEditor.selected;
+ if (!item || !action) {
+ return;
+ }
+ const targets = { up: item.order + 1, down: item.order - 1, top: item.count - 1, bottom: 0 };
+ return changeDocument("ofdgoMoveObject", item, targets[action]);
+});
el.undoButton.addEventListener("click", () => changeDocument("ofdgoUndo"));
el.redoButton.addEventListener("click", () => changeDocument("ofdgoRedo"));
el.addPageButton.addEventListener("click", () => {
@@ -720,6 +743,9 @@ async function changeDocument(name, item, ...args) {
if (!item || name === "ofdgoDeleteObject") {
canvasEditor.clear();
}
+ if (name === "ofdgoCopyObject") {
+ canvasEditor.selectLast = true;
+ }
openSeq = ++state.openSeq;
if (item) {
await refreshEditorPage(doc, item.index, openSeq);
@@ -3771,8 +3797,13 @@ function updateEditorTools() {
}
function updateObjectControls(item) {
- el.deleteObjectButton.disabled = !item || !state.ready || state.exporting;
+ const disabled = !item || !state.ready || state.exporting;
+ el.deleteObjectButton.disabled = el.copyObjectButton.disabled = disabled;
el.editTextButton.disabled = !item || item.image || !state.ready || state.exporting;
+ el.objectOrder.disabled = disabled || item.count <= 1;
+ for (const option of el.objectOrder.options) {
+ option.disabled = disabled || (["up", "top"].includes(option.value) ? item.order === item.count - 1 : item.order === 0);
+ }
}
function updateAnnotationButton() {
diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go
index 84f47a9..2c7b5c8 100644
--- a/internal/webui/wasm.go
+++ b/internal/webui/wasm.go
@@ -101,6 +101,8 @@ func RunWASM() {
registerCallback("ofdgoUpdateText", updateText)
registerCallback("ofdgoInsertImage", insertImage)
registerCallback("ofdgoTransformObject", transformObject)
+ registerCallback("ofdgoCopyObject", copyObject)
+ registerCallback("ofdgoMoveObject", moveObject)
registerCallback("ofdgoDeleteObject", deleteObject)
registerCallback("ofdgoUndo", func([]js.Value) (any, error) { return restoreEditor(false) })
registerCallback("ofdgoRedo", func([]js.Value) (any, error) { return restoreEditor(true) })
@@ -589,7 +591,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
fontNames[font.ID] = font.FontName
}
for _, layer := range page.Content.Layer {
- for _, object := range layer.Objects {
+ for order, object := range layer.Objects {
var id string
var box ofdgo.Box
switch object.Type {
@@ -606,7 +608,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
continue
}
if box.W > 0 && box.H > 0 {
- item := map[string]any{"id": id, "image": object.Type == "ImageObject", "x": box.X, "y": box.Y, "width": box.W, "height": box.H}
+ item := map[string]any{"id": id, "image": object.Type == "ImageObject", "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects)}
if object.Type == "TextObject" {
item["text"], item["font"], item["fontName"], item["size"] = textValues[id], object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size
}
@@ -654,6 +656,55 @@ func transformObject(args []js.Value) (any, error) {
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
+// copyObject 复制选中对象并平移,复用字体和图片资源
+// 入参: args 页码、对象标识和位移
+// 返回: any 文档信息, error 错误信息
+func copyObject(args []js.Value) (any, error) {
+ if currentEditor == nil {
+ return nil, fmt.Errorf("no document is being created")
+ }
+ page := args[0].Int()
+ object, err := currentEditor.Object(page, args[1].String())
+ if err != nil {
+ return nil, err
+ }
+ var boundary *string
+ switch object.Type {
+ case "TextObject":
+ boundary = &object.TextObject.Boundary
+ case "ImageObject":
+ boundary = &object.ImageObject.Boundary
+ default:
+ return nil, fmt.Errorf("unsupported object type %q", object.Type)
+ }
+ box, err := ofdgo.ParseBox(*boundary)
+ if err != nil {
+ return nil, err
+ }
+ *boundary = fmt.Sprintf("%g %g %g %g", box.X+args[2].Float(), box.Y+args[3].Float(), box.W, box.H)
+ if _, err := currentEditor.AddObject(page, object); err != nil {
+ return nil, err
+ }
+ return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
+}
+
+// moveObject 调整选中对象的绘制顺序
+// 入参: args 页码、对象标识和目标索引
+// 返回: any 文档信息, error 错误信息
+func moveObject(args []js.Value) (any, error) {
+ if currentEditor == nil {
+ return nil, fmt.Errorf("no document is being created")
+ }
+ revision := currentEditor.Revision()
+ if err := currentEditor.MoveObject(args[0].Int(), args[1].String(), args[2].Int()); err != nil {
+ return nil, err
+ }
+ if currentEditor.Revision() == revision {
+ return editorSummary(), nil
+ }
+ return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
+}
+
// deleteObject 删除创作画布中选中的对象
// 入参: args 页码和对象标识
// 返回: any 文档信息, error 错误信息