diff --git a/assets/webui/index.html b/assets/webui/index.html
index 395497e..ec8ebad 100644
--- a/assets/webui/index.html
+++ b/assets/webui/index.html
@@ -70,6 +70,7 @@
对象
-
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css
index f5abf09..9982869 100644
--- a/assets/webui/ofdgo.css
+++ b/assets/webui/ofdgo.css
@@ -108,11 +108,15 @@ a:focus-visible {
grid-template-columns: repeat(5, max-content);
}
-.editor-group:has(#insertTextButton),
-.editor-group:has(#textSize) {
+.editor-group:has(#insertTextButton) {
grid-template-columns: repeat(2, max-content);
}
+#textFont {
+ width: 128px;
+ min-width: 0;
+}
+
.editor-group:has(#addPageButton) {
grid-template-columns: repeat(6, max-content);
}
@@ -308,14 +312,20 @@ button:not(:disabled):hover {
padding: 0;
border: 0;
outline: 0;
- appearance: textfield;
text-align: center;
color: #1f2428;
background: transparent;
}
+.page-control input,
+.text-size input {
+ appearance: textfield;
+}
+
.page-control input::-webkit-inner-spin-button,
-.page-control input::-webkit-outer-spin-button {
+.page-control input::-webkit-outer-spin-button,
+.text-size input::-webkit-inner-spin-button,
+.text-size input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js
index 15c7f14..b1b0986 100644
--- a/assets/webui/ofdgo.js
+++ b/assets/webui/ofdgo.js
@@ -38,6 +38,7 @@ const state = {
savedRevision: null,
textObject: null,
insertFonts: [],
+ textFonts: [],
ofdBytes: null,
fileName: "ofdgo.ofd",
openSeq: 0,
@@ -89,6 +90,7 @@ const el = {
editTextButton: document.querySelector("#editTextButton"),
copyObjectButton: document.querySelector("#copyObjectButton"),
objectOrder: document.querySelector("#objectOrder"),
+ textFont: document.querySelector("#textFont"),
textSize: document.querySelector("#textSize"),
textColor: document.querySelector("#textColor"),
undoButton: document.querySelector("#undoButton"),
@@ -121,14 +123,10 @@ const el = {
insertForm: document.querySelector("#insertForm"),
insertTextRow: document.querySelector("#insertTextRow"),
insertFontRow: document.querySelector("#insertFontRow"),
- insertSizeRow: document.querySelector("#insertSizeRow"),
- insertColorRow: document.querySelector("#insertColorRow"),
insertImageRow: document.querySelector("#insertImageRow"),
insertText: document.querySelector("#insertText"),
insertFont: document.querySelector("#insertFont"),
insertFontAdd: document.querySelector("#insertFontAdd"),
- insertSize: document.querySelector("#insertSize"),
- insertColor: document.querySelector("#insertColor"),
insertImage: document.querySelector("#insertImage"),
insertStatus: document.querySelector("#insertStatus"),
insertCancel: document.querySelector("#insertCancel"),
@@ -228,7 +226,8 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
busy: () => document.body.hasAttribute("aria-busy"),
rotation: () => state.rotation,
onSelect: updateObjectControls,
- onTransform: (item, change) => changeDocument("ofdgoTransformObject", item, change.x, change.y, change.scale),
+ onTransform: (item, change) => changeDocument("ofdgoTransformObject", item,
+ change.x + item.x * (1 - change.scale), change.y + item.y * (1 - change.scale), change.scale),
onDelete: (item) => changeDocument("ofdgoDeleteObject", item),
onEdit: (item) => openInsertPanel(true, item),
});
@@ -255,6 +254,13 @@ el.editTextButton.addEventListener("click", () => {
});
el.textSize.addEventListener("change", () => changeTextStyle(false));
el.textColor.addEventListener("change", () => changeTextStyle(true));
+el.textFont.addEventListener("change", () => {
+ const item = canvasEditor.selected;
+ const font = state.textFonts[Number(el.textFont.value)];
+ if (item && !item.image && state.ready && !document.body.hasAttribute("aria-busy") && font && !font.embedded) {
+ return changeDocument("ofdgoUpdateText", item, item.text, fontManager.read(font), item.size, null);
+ }
+});
el.textSize.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === "Escape") {
event.preventDefault();
@@ -643,12 +649,10 @@ async function openInsertPanel(text, item = null) {
if (item) {
state.insertFonts = [];
el.insertText.value = item.text;
- el.insertSize.value = textPoints(item.size);
- el.insertColor.value = item.color;
}
el.insertPanel.setAttribute("aria-label", item ? "修改文字" : text ? "添加文字" : "添加图片");
el.insertSubmit.textContent = item ? "修改" : "添加";
- for (const [row, input] of [[el.insertTextRow, el.insertText], [el.insertFontRow, el.insertFont], [el.insertSizeRow, el.insertSize], [el.insertColorRow, el.insertColor]]) {
+ for (const [row, input] of [[el.insertTextRow, el.insertText], [el.insertFontRow, el.insertFont]]) {
row.hidden = !text;
input.disabled = !text;
}
@@ -661,21 +665,39 @@ async function openInsertPanel(text, item = null) {
function updateInsertFonts() {
const selected = state.insertFonts[Number(el.insertFont.value)];
- const key = (font) => font?.id || font?.postscriptName;
- state.insertFonts = [...fontManager.userFonts.filter((font) => font.enabled), ...fontManager.catalog];
- if (state.textObject) {
- state.insertFonts.unshift({ id: `embedded:${state.textObject.font}`, name: state.textObject.fontName, embedded: true });
+ state.insertFonts = editorFonts(state.textObject);
+ setFontOptions(el.insertFont, state.insertFonts, selected);
+ el.insertSubmit.disabled = !el.insertTextRow.hidden && !state.insertFonts.length;
+ el.insertStatus.textContent = el.insertSubmit.disabled ? "尚未添加字体" : "";
+}
+
+function editorFonts(item) {
+ const fonts = [...fontManager.userFonts.filter((font) => font.enabled), ...fontManager.catalog];
+ if (item) {
+ fonts.unshift({ id: `embedded:${item.font}`, name: item.fontName, embedded: true });
}
- el.insertFont.replaceChildren();
- state.insertFonts.forEach((font, index) => {
+ return fonts;
+}
+
+function setFontOptions(select, fonts, selected) {
+ const key = (font) => font?.id || font?.postscriptName;
+ select.replaceChildren();
+ fonts.forEach((font, index) => {
const option = document.createElement("option");
option.value = String(index);
option.textContent = font.fullName || font.name;
option.selected = key(font) === key(selected);
- el.insertFont.append(option);
+ select.append(option);
});
- el.insertSubmit.disabled = !el.insertTextRow.hidden && !state.insertFonts.length;
- el.insertStatus.textContent = el.insertSubmit.disabled ? "尚未添加字体" : "";
+}
+
+function updateTextFonts(item, refresh = false) {
+ const text = item && !item.image;
+ if (refresh || state.textFonts[0]?.id !== (text ? `embedded:${item.font}` : undefined)) {
+ state.textFonts = text ? editorFonts(item) : [];
+ setFontOptions(el.textFont, state.textFonts, state.textFonts[0]);
+ }
+ el.textFont.value = text ? "0" : "";
}
async function insertObject(event) {
@@ -705,11 +727,10 @@ async function insertObject(event) {
if (openSeq !== state.openSeq) {
return;
}
- const points = Number(el.insertSize.value);
- const size = item && points === textPoints(item.size) ? item.size : points * 25.4 / 72;
+ const value = item && data === null && el.insertText.value === item.text ? null : el.insertText.value;
const doc = item
- ? await callWASM("ofdgoUpdateText", item.index, item.id, el.insertText.value, data, size, el.insertColor.value === item.color ? null : el.insertColor.value)
- : text ? await callWASM("ofdgoInsertText", index, el.insertText.value, data, x, y, size, el.insertColor.value)
+ ? await callWASM("ofdgoUpdateText", item.index, item.id, value, data, item.size, null)
+ : text ? await callWASM("ofdgoInsertText", index, el.insertText.value, data, x, y, 12 * 25.4 / 72, "#000000")
: await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2);
if (openSeq !== state.openSeq) {
return;
@@ -744,7 +765,7 @@ async function insertObject(event) {
}
function textPoints(size) {
- return Number((size * 72 / 25.4).toPrecision(12));
+ return Number((size * 72 / 25.4).toFixed(2));
}
async function changeTextStyle(color) {
@@ -774,6 +795,10 @@ async function changeDocument(name, item, ...args) {
const { scrollLeft, scrollTop } = el.viewerPanel;
setBusy(true);
try {
+ args = await Promise.all(args);
+ if (openSeq !== state.openSeq) {
+ return;
+ }
const doc = await callWASM(name, ...(item ? [item.index, item.id] : []), ...args);
if (openSeq !== state.openSeq) {
return;
@@ -1579,6 +1604,7 @@ async function changeFont(font, changes) {
}
function renderFontList() {
+ updateTextFonts(canvasEditor.selected, true);
if (el.insertPanel.open) {
updateInsertFonts();
}
@@ -3842,7 +3868,8 @@ function updateObjectControls(item, reset = false) {
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.textSize.disabled = el.textColor.disabled = el.editTextButton.disabled;
+ el.textFont.disabled = el.textSize.disabled = el.textColor.disabled = el.editTextButton.disabled;
+ updateTextFonts(item);
if (reset || el.textSize.disabled || document.activeElement !== el.textSize) {
el.textSize.value = item && !item.image ? textPoints(item.size) : "";
}
diff --git a/assets/webui/ofdgo_edit.js b/assets/webui/ofdgo_edit.js
index 3150a33..2e926a3 100644
--- a/assets/webui/ofdgo_edit.js
+++ b/assets/webui/ofdgo_edit.js
@@ -74,13 +74,11 @@ export class CanvasEditor {
this.nodes.set(node, item);
node.addEventListener("focus", () => this.select(item));
this.place(item);
- if (object.image) {
- for (const corner of ["nw", "ne", "sw", "se"]) {
- const handle = document.createElement("span");
- handle.className = `edit-handle edit-${corner}`;
- handle.dataset.corner = corner;
- node.append(handle);
- }
+ for (const corner of ["nw", "ne", "sw", "se"]) {
+ const handle = document.createElement("span");
+ handle.className = `edit-handle edit-${corner}`;
+ handle.dataset.corner = corner;
+ node.append(handle);
}
layer.append(node);
if (this.selected?.index === index && this.selected.id === object.id) {
diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go
index a76665b..aed52bc 100644
--- a/internal/webui/wasm.go
+++ b/internal/webui/wasm.go
@@ -627,7 +627,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
return objects, nil
}
-// transformObject 移动文字或等比缩放图片
+// transformObject 等比缩放后平移对象
// 入参: args 页码、对象标识、位移和缩放比例
// 返回: any 文档信息, error 错误信息
func transformObject(args []js.Value) (any, error) {
@@ -635,31 +635,12 @@ func transformObject(args []js.Value) (any, error) {
return nil, fmt.Errorf("no document is being created")
}
page, id := args[0].Int(), args[1].String()
- object, err := currentEditor.Object(page, id)
- if err != nil {
+ revision := currentEditor.Revision()
+ if err := currentEditor.TransformObject(page, id, args[2].Float(), args[3].Float(), args[4].Float()); err != nil {
return nil, err
}
- scale := args[4].Float()
- var boundary *string
- switch object.Type {
- case "TextObject":
- if scale != 1 {
- return nil, fmt.Errorf("text resizing is not supported")
- }
- boundary = &object.TextObject.Boundary
- case "ImageObject":
- boundary = &object.ImageObject.Boundary
- object.ImageObject.CTM = ""
- 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*scale, box.H*scale)
- if err := currentEditor.UpdateObject(page, id, object); err != nil {
- return nil, err
+ if currentEditor.Revision() == revision {
+ return editorSummary(), nil
}
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
diff --git a/ofdgo_editor.go b/ofdgo_editor.go
index fcfb704..948c5db 100644
--- a/ofdgo_editor.go
+++ b/ofdgo_editor.go
@@ -652,6 +652,73 @@ func (e *Editor) UpdateText(page int, id, value, fontID string, size float64) er
return nil
}
+// TransformObject 以页面原点等比缩放后平移对象,文字同步缩放字号和字距,保留原有排版
+// 入参: page 页面索引, id 对象标识, dx 横向位移, dy 纵向位移, scale 正缩放比例
+// 返回: error 错误信息
+func (e *Editor) TransformObject(page int, id string, dx, dy, scale float64) error {
+ if !finite(dx) || !finite(dy) || !finite(scale) || scale <= 0 {
+ return fmt.Errorf("transform requires finite offsets and a positive finite scale")
+ }
+ object, err := e.Object(page, id)
+ if err != nil {
+ return err
+ }
+ if dx == 0 && dy == 0 && scale == 1 {
+ return nil
+ }
+ var boundary, ctm *string
+ switch object.Type {
+ case "TextObject":
+ obj := &object.TextObject
+ boundary, ctm = &obj.Boundary, &obj.CTM
+ if scale != 1 {
+ obj.Size *= scale
+ if obj.LineWidth == 0 && obj.Stroke != nil && *obj.Stroke {
+ obj.LineWidth = defaultPathLineWidth
+ }
+ obj.LineWidth *= scale
+ for i := range obj.TextCode {
+ code := &obj.TextCode[i]
+ for _, value := range []*string{&code.X, &code.Y, &code.DeltaX, &code.DeltaY} {
+ *value = scaleTextNumbers(*value, scale)
+ }
+ }
+ }
+ case "PathObject":
+ boundary, ctm = &object.PathObject.Boundary, &object.PathObject.CTM
+ case "ImageObject":
+ boundary, ctm = &object.ImageObject.Boundary, &object.ImageObject.CTM
+ }
+ box, err := ParseBox(*boundary)
+ if err != nil {
+ return err
+ }
+ *boundary = fmt.Sprintf("%s %s %s %s", ofdNumber(box.X*scale+dx), ofdNumber(box.Y*scale+dy), ofdNumber(box.W*scale), ofdNumber(box.H*scale))
+ if scale != 1 && (*ctm != "" || object.Type != "TextObject") {
+ m := NewMatrix(*ctm)
+ if object.Type != "TextObject" {
+ m.a, m.b, m.c, m.d = m.a*scale, m.b*scale, m.c*scale, m.d*scale
+ }
+ *ctm = fmt.Sprintf("%s %s %s %s %s %s", ofdNumber(m.a), ofdNumber(m.b), ofdNumber(m.c), ofdNumber(m.d), ofdNumber(m.e*scale), ofdNumber(m.f*scale))
+ }
+ return e.UpdateObject(page, id, object)
+}
+
+// scaleTextNumbers 缩放已校验的文字定位数值,保留g重复编码
+// 入参: value 定位数值, scale 缩放比例
+// 返回: string 缩放后的数值
+func scaleTextNumbers(value string, scale float64) string {
+ fields := strings.Fields(value)
+ for i := 0; i < len(fields); i++ {
+ if fields[i] == "g" {
+ i += 2
+ }
+ number, _ := strconv.ParseFloat(fields[i], 64)
+ fields[i] = ofdNumber(number * scale)
+ }
+ return strings.Join(fields, " ")
+}
+
// LayoutText 按嵌入字体度量重排横向单行文字,设置基线和字距,保留绘制属性
// 不进行段落排版和复杂文字塑形,不修改文档,通过AddObject或UpdateObject提交
// 入参: obj 文字对象, value 原文