From f246b0f54319c09fd3463b376e85ea9e72e8cc07 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Tue, 27 Oct 2026 22:38:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E7=BC=96=E8=BE=91=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=87=E6=9C=AC=E5=AF=B9=E9=BD=90=E4=B8=8E?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=81=8F=E5=B7=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 8 +- assets/webui/ofdgo_edit.js | 4 +- internal/webui/webui.go | 32 ++---- ofdgo_editor_rich_text.go | 213 ++++++++++++++++++++++--------------- 4 files changed, 144 insertions(+), 113 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index dd23ef3..a663941 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -4142,8 +4142,8 @@ async function loadRichText(item, firstFace, tab = documentTab(), firstData = nu if (tab.closed || wasmSeq !== tab.state.wasmSeq) throw new Error("文档已更改"); return callTabWASM(tab, name, ...args); }; - const prepared = reflow ? await callWASM("ofdgoGetRichText", item.index, item.id, true) : null; - const model = prepared?.model || item.richText || await callWASM("ofdgoGetRichText", item.index, item.id); + const prepared = item.richText ? { model: item.richText, frame: item.textFrame } : await callWASM("ofdgoGetRichText", item.index, item.id, reflow); + const model = prepared.model; const remaining = firstData ? { ...model, runs: model.runs.filter(run => run.format.font !== item.font) } : model; const fontData = remaining.runs.length ? await callWASM("ofdgoRichTextFonts", JSON.stringify(remaining)) : []; const resources = new Map(fontData.map(font => [font.key, font.data])); @@ -4155,7 +4155,7 @@ async function loadRichText(item, firstFace, tab = documentTab(), firstData = nu if (tab.closed || wasmSeq !== tab.state.wasmSeq) throw new Error("文档已更改"); faces.set(id, face); } - return { model, faces, resources, frame: prepared?.frame }; + return { model, faces, resources, frame: prepared.frame, reflow }; } async function ensureRichInput(reflow = false) { @@ -4256,7 +4256,7 @@ async function validateRichText(item, model, width, fonts = []) { async function saveRichText(item, model, fonts = [], box = item) { const tab = documentTab(), openSeq = state.openSeq, wasmSeq = state.wasmSeq; const boundary = { x: box.x, y: box.y, width: box.width, height: box.height }; - if (item.richReflow) { boundary.reflow = true; boundary.width = item.textFrame.width; } + if (item.richReflow) boundary.reflow = true; if (box.frameWidth !== undefined) Object.assign(boundary, { frameWidth: box.frameWidth, frameOffset: box.frameOffset || 0 }); fonts = await validateRichText(item, model, box.frameWidth ?? item.textFrame?.width ?? item.width, fonts); if (tab.closed || !isForegroundDocument(tab) || tab.state.openSeq !== openSeq || tab.state.wasmSeq !== wasmSeq) return false; diff --git a/assets/webui/ofdgo_edit.js b/assets/webui/ofdgo_edit.js index a5dbc06..9928c57 100644 --- a/assets/webui/ofdgo_edit.js +++ b/assets/webui/ofdgo_edit.js @@ -1958,12 +1958,10 @@ export class CanvasEditor { const value = editTextValue(editing.input); const session = new RichTextSession(this, rich.model, rich.faces, rich.fonts, rich.resources); const base = { ...session.typing }, fonts = new Map(); - const scale = rich.frame ? sourceTextScale(editing) : 1; const current = { value, style: editing.style, face: editing.face, fontData: editing.fontData, fontChoice: editing.fontChoice, caret }; const values = editing.history ? editing.history.entries.slice(0, editing.history.index + 1) : [{ value: editing.item.text }]; for (const entry of [...values, current]) { const format = { ...base, ...entry.style }; - if (entry.style?.size) format.size *= scale; if (entry.fontData) { let key = fonts.get(entry.fontData); if (!key) { @@ -1982,7 +1980,7 @@ export class CanvasEditor { } if (rich.frame) { editing.frame = rich.frame; - editing.item = { ...editing.item, richReflow: true, textFrame: rich.frame }; + editing.item = { ...editing.item, richReflow: rich.reflow, textFrame: rich.frame }; Object.assign(editing.input.style, { whiteSpace: rich.model.layout.wrap ? "pre-wrap" : "pre", textIndent: "0px", letterSpacing: "0px" }); } editing.rich = session; diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 898a299..7f0cefd 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -5002,38 +5002,24 @@ func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.Graphi // getRichText 读取文字分段与段落选项,颜色转换为浏览器RGB格式 // 入参: args 页码、对象标识和可选显式重排标记 -// 返回: any 富文本快照或重排模型及页面坐标框, error 错误信息 +// 返回: any 富文本快照,携带重排标记时附带局部编辑框及页面变换, error 错误信息 func (e *businessEngine) getRichText(args []platformValue) (any, error) { if e.currentEditor == nil { return nil, fmt.Errorf("no document is being edited") } - if len(args) > 2 && args[2].Bool() { - selection, err := e.readTextSelection([]string{args[1].String()}) - if err != nil { - return nil, err - } - analysis, err := e.currentEditor.AnalyzeTextSelection(args[0].Int(), selection) - if err != nil { - return nil, err - } - if !analysis.Reflow { - return nil, fmt.Errorf("text cannot be reflowed") - } - value := analysis.RichText - value.Layout.AutoBidi = true - box := analysis.Bounds - return map[string]any{"model": e.browserRichText(value), "frame": map[string]any{ - "width": box.W, "height": box.H, "matrix": e.editorMatrix(ofdgo.TranslationMatrix(box.X, box.Y)), - }}, nil - } path, err := e.compositePath(args[1].String()) if err != nil { return nil, err } - value, err := e.currentEditor.TargetRichText(args[0].Int(), path) + value, frame, matrix, err := e.currentEditor.PrepareRichText(args[0].Int(), path, len(args) > 2 && args[2].Bool()) if err != nil { return nil, err } + if len(args) > 2 { + return map[string]any{"model": e.browserRichText(value), "frame": map[string]any{ + "width": frame.W, "height": frame.H, "matrix": e.editorMatrix(matrix), + }}, nil + } return e.browserRichText(value), nil } @@ -5150,11 +5136,11 @@ func (e *businessEngine) saveRichText(args []platformValue) (any, error) { } box := args[3] if !box.IsNull() && !box.IsUndefined() && box.Get("reflow").Type() == platformTypeBoolean && box.Get("reflow").Bool() { - selection, err := e.readTextSelection([]string{id}) + path, err := e.compositePath(id) if err != nil { return err } - return e.currentEditor.UpdateTextSelection(page, selection, ofdgo.TextSelectionEdit{Reflow: &value, Width: box.Get("width").Float()}) + return e.currentEditor.ReflowRichText(page, path, value) } if len(args) > 5 && args[5].String() != "" { key := args[5].String() diff --git a/ofdgo_editor_rich_text.go b/ofdgo_editor_rich_text.go index b004284..ed13059 100644 --- a/ofdgo_editor_rich_text.go +++ b/ofdgo_editor_rich_text.go @@ -101,7 +101,7 @@ func (e *Editor) AddRichText(page int, box Box, value RichText) (string, error) // 入参: page 页面索引, id 对象标识, value 完整富文本 // 返回: error 识别、排版或提交错误 func (e *Editor) UpdateRichText(page int, id string, value RichText) error { - return e.updateRichTextFrame(page, id, value, 0, 0) + return e.updateRichTextFrame(page, id, value, 0, 0, false) } // ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放 @@ -115,23 +115,19 @@ func (e *Editor) ResizeRichTextFrame(page int, id string, offset, width float64) if err != nil { return err } - return e.updateRichTextFrame(page, id, value, offset, width) + return e.updateRichTextFrame(page, id, value, offset, width, false) } // updateRichTextFrame 统一提交正文或编辑框变化,width为零时保持原始宽度 -// 入参: page 页面索引, id 对象标识, value 新内容, offset 局部偏移, width 可选宽度 +// 入参: page 页面索引, id 对象标识, value 新内容, offset 局部偏移, width 可选宽度, reflow 是否显式重排原文 // 返回: error 校验或提交错误 -func (e *Editor) updateRichTextFrame(page int, id string, value RichText, offset, width float64) error { +func (e *Editor) updateRichTextFrame(page int, id string, value RichText, offset, width float64, reflow bool) error { return e.Transaction(func(e *Editor) error { old, err := e.Object(page, id) if err != nil { return err } - current, err := e.RichText(page, id) - if err != nil { - return err - } - frame, matrix, err := e.RichTextFrame(page, id) + current, frame, matrix, err := e.PrepareRichText(page, ObjectPath{ID: id}, reflow) if err != nil { return err } @@ -262,19 +258,8 @@ func (e *Editor) RichTextFrame(page int, id string) (Box, Matrix, error) { return Box{}, Matrix{}, err } if object.Type == "TextObject" { - if _, err := e.promoteRichText(page, object); err != nil { - return Box{}, Matrix{}, err - } - frame, err := object.TextObject.TextFrame() - if err != nil { - return Box{}, Matrix{}, err - } - box, _ := creationBox(object.TextObject.Boundary) - dx, dy, err := e.richPlainOffset(object.TextObject) - if err != nil { - return Box{}, Matrix{}, err - } - return Box{W: frame.W, H: frame.H}, TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.TextObject.CTM)).Multiply(TranslationMatrix(frame.X+dx, frame.Y+dy)), nil + _, frame, matrix, err := e.PrepareRichText(page, ObjectPath{ID: id}, false) + return frame, matrix, err } if object.Type != "CompositeObject" { return Box{}, Matrix{}, ErrNotRichText @@ -341,6 +326,13 @@ func (e *Editor) CompositeRichText(page int, member CompositeMember) (RichText, // 入参: page 页面索引, path 父路径, index 成员序号, value 富文本, offset 局部横向偏移, width 新段落宽度,零值保留 // 返回: error 校验或提交错误 func (e *Editor) UpdateCompositeRichText(page int, path ObjectPath, index int, value RichText, offset, width float64) error { + return e.updateCompositeRichText(page, path, index, value, offset, width, false) +} + +// updateCompositeRichText 统一处理内部文字的安全提升与显式重排 +// 入参: page 页面索引, path 父路径, index 成员序号, value 富文本, offset 局部偏移, width 可选宽度, reflow 是否显式重排 +// 返回: error 校验或提交错误 +func (e *Editor) updateCompositeRichText(page int, path ObjectPath, index int, value RichText, offset, width float64, reflow bool) error { if !finite(offset) || !finite(width) || width < 0 || width == 0 && offset != 0 { return fmt.Errorf("invalid rich text frame") } @@ -350,7 +342,7 @@ func (e *Editor) UpdateCompositeRichText(page int, path ObjectPath, index int, v return err } if member.Object.Type == "TextObject" { - return e.updateCompositePlainRichText(page, path, index, value, offset, width) + return e.updateCompositePlainRichText(page, path, index, value, offset, width, reflow) } var metadata richTextMetadata entryIndex := -1 @@ -677,9 +669,13 @@ func (e *Editor) promoteRichText(page int, object GraphicObject) (RichText, erro if err != nil { return RichText{}, err } - if origin := e.objectOrigin(object.TextObject.ID); layer.DrawParam != "" || origin != nil && !editorXMLSupported(origin.node) { + if origin := e.objectOrigin(object.TextObject.ID); origin != nil && !editorXMLSupported(origin.node) { return RichText{}, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("text cannot be safely promoted to rich text")} } + object, err = e.resolveEditorStyle(object, layer.DrawParam) + if err != nil { + return RichText{}, err + } return e.promoteTextValue(object.TextObject) } @@ -693,11 +689,7 @@ func (e *Editor) promoteTextValue(t TextObject) (RichText, error) { if t.DrawParam != "" || t.Clips != nil || len(t.Actions) != 0 || t.Alpha != nil && *t.Alpha != 255 || t.Visible != nil && !*t.Visible || t.Stroke != nil && *t.Stroke || t.Fill != nil && !*t.Fill || t.Decoration != "" || t.VScale != 0 || t.HScale != 0 && t.HScale != 1 || t.ReadDirection != 0 || t.CharDirection != 0 || t.Weight != 0 && t.Weight != 400 && t.Weight != 700 { return unsupported() } - frame, err := t.TextFrame() - if err != nil { - return unsupported() - } - if frame.X != 0 || frame.Y != 0 || t.CTM != "" && NewMatrix(t.CTM) != (IdentityMatrix) { + if _, err := t.TextFrame(); err != nil { return unsupported() } value, layout := t.TextLayout() @@ -713,7 +705,7 @@ func (e *Editor) promoteTextValue(t TextObject) (RichText, error) { if actual.Index != "" || len(textCodeRunes(actual.Value)) != len(textCodeRunes(expected.Value)) { return unsupported() } - if actual.DeltaX != "" && !reflect.DeepEqual(parseFloatsWithG(actual.DeltaX), parseFloatsWithG(expected.DeltaX)) { + if !reflect.DeepEqual(parseFloatsWithG(actual.DeltaX), parseFloatsWithG(expected.DeltaX)) { return unsupported() } for _, dy := range parseFloatsWithG(actual.DeltaY) { @@ -745,18 +737,16 @@ func (e *Editor) promoteTextValue(t TextObject) (RichText, error) { return result, result.Validate() } -// richPlainOffset 保存可提升单行原文的基线偏移,不推断复杂定位布局 +// richPlainOffset 计算原首行相对标准排版的位移,保留既有段落和组合内基线 // 入参: text 已校验的普通文字 // 返回: float64 横向偏移, float64 基线偏移, error 无效坐标 func (e *Editor) richPlainOffset(text TextObject) (float64, float64, error) { - if text.layout != nil { - return 0, 0, nil - } - if len(text.TextCode) != 1 { - return 0, 0, fmt.Errorf("positioned text requires explicit layout") + if len(text.TextCode) == 0 { + return 0, 0, fmt.Errorf("text codes are empty") } + value, layout := text.TextLayout() check := text - if err := e.LayoutText(&check, text.Text(), TextLayout{}); err != nil { + if err := e.LayoutText(&check, value, layout); err != nil { return 0, 0, err } x, err := strconv.ParseFloat(text.TextCode[0].X, 64) @@ -771,7 +761,11 @@ func (e *Editor) richPlainOffset(text TextObject) (float64, float64, error) { if err != nil { return 0, 0, err } - return x, y - baseline, nil + origin, err := strconv.ParseFloat(check.TextCode[0].X, 64) + if err != nil { + return 0, 0, err + } + return x - origin, y - baseline, nil } // copyRichTextMetadata 迁移选区及其组合、注解内部的编辑标引,复用标准内容中的字体引用 @@ -1078,49 +1072,34 @@ func (e *Editor) formatCompositeText(page int, path ObjectPath, index int, patch } // updateCompositePlainRichText 将可验证布局的内部文字提升为富文本并保留实例坐标 -// 入参: page 页面索引, path 父路径, index 成员序号, value 富文本内容, offset 局部偏移, width 可选宽度 +// 入参: page 页面索引, path 父路径, index 成员序号, value 富文本内容, offset 局部偏移, width 可选宽度, reflow 是否显式重排 // 返回: error 校验或提交错误 -func (e *Editor) updateCompositePlainRichText(page int, path ObjectPath, index int, value RichText, offset, width float64) error { +func (e *Editor) updateCompositePlainRichText(page int, path ObjectPath, index int, value RichText, offset, width float64, reflow bool) error { + target := ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: append(slices.Clone(path.Children), index)} + _, frame, matrix, err := e.PrepareRichText(page, target, reflow) + if err != nil { + return err + } + matrix = matrix.Multiply(TranslationMatrix(offset, 0)) + if width > 0 { + frame.W = width + } var metadata richTextMetadata if err := e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error { - node, text := nodes[0], members[0].Object.TextObject + node := nodes[0] if node.object.Type != "TextObject" || !editorXMLSupported(node.span) { return ErrNotRichText } - text.CTM = "" - _, err := e.promoteTextValue(text) - if err != nil { - return err - } - if err := value.Validate(); err != nil { - return err - } - frame, err := text.TextFrame() - if err != nil { - return err - } - dx, dy, err := e.richPlainOffset(text) - if err != nil { - return err - } - object := node.object - if !node.boundaryInCTM { - object = compositeBoundary(object, node.parent, true) - } - box, err := creationBox(object.TextObject.Boundary) - if err != nil { - return err - } - matrix := TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.TextObject.CTM)).Multiply(TranslationMatrix(dx+offset, dy)) - if width > 0 { - frame.W = width + inverse, ok := node.parent.Invert() + if !ok { + return fmt.Errorf("invalid composite parent transform") } next, meta, err := e.prepareRichText(Box{W: frame.W, H: frame.H}, value) if err != nil { return err } - unit := richTextPlacement(meta, matrix) - unit.ID, unit.ResourceID = object.TextObject.ID, next.CompositeGraphicUnit.ResourceID + unit := richTextPlacement(meta, inverse.Multiply(matrix)) + unit.ID, unit.ResourceID = node.object.TextObject.ID, next.CompositeGraphicUnit.ResourceID next.CompositeGraphicUnit = unit if !node.boundaryInCTM { next = compositeBoundary(next, node.parent, false) @@ -1141,23 +1120,91 @@ func (e *Editor) updateCompositePlainRichText(page int, path ObjectPath, index i // 入参: page 页面索引, path 完整对象路径 // 返回: RichText 独立模型, error 路径或不安全提升错误 func (e *Editor) TargetRichText(page int, path ObjectPath) (RichText, error) { + value, _, _, err := e.PrepareRichText(page, path, false) + return value, err +} + +// PrepareRichText 读取独立富文本模型和局部编辑框,显式重排时保留原首行起点及页面变换 +// 入参: page 页面索引, path 完整对象路径, reflow 是否允许重排普通文字 +// 返回: RichText 内容, Box 局部编辑框, Matrix 页面变换, error 路径、外观或排版错误 +func (e *Editor) PrepareRichText(page int, path ObjectPath, reflow bool) (RichText, Box, Matrix, error) { + var text TextObject + var matrix Matrix if len(path.Children) == 0 { if path.Annotation != "" { - return RichText{}, ErrNotRichText + return RichText{}, Box{}, Matrix{}, ErrNotRichText } - return e.RichText(page, path.ID) + object, err := e.Object(page, path.ID) + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + if object.Type != "TextObject" { + value, err := e.RichText(page, path.ID) + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + frame, matrix, err := e.RichTextFrame(page, path.ID) + return value, frame, matrix, err + } + if origin := e.objectOrigin(path.ID); origin != nil && !editorXMLSupported(origin.node) { + return RichText{}, Box{}, Matrix{}, ErrNotRichText + } + layer, _, err := e.findObject(page, path.ID) + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + object, err = e.resolveEditorStyle(object, layer.DrawParam) + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + text = object.TextObject + box, err := creationBox(text.Boundary) + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + matrix = TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(text.CTM)) + } else { + index := path.Children[len(path.Children)-1] + path.Children = path.Children[:len(path.Children)-1] + member, err := e.CompositeObject(page, path, index) + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + if member.Object.Type != "TextObject" { + return e.CompositeRichText(page, member) + } + text, matrix = member.Object.TextObject, member.Matrix + } + frame, err := text.TextFrame() + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + dx, dy, err := e.richPlainOffset(text) + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + if reflow { + value, layout := text.TextLayout() + layout.AutoBidi = true + if err := e.LayoutText(&text, value, layout); err != nil { + return RichText{}, Box{}, Matrix{}, err + } + } + value, err := e.promoteTextValue(text) + return value, Box{W: frame.W, H: frame.H}, matrix.Multiply(TranslationMatrix(dx, dy)), err +} + +// ReflowRichText 原子重排完整路径文字,保留局部宽度、页面变换及共享实例隔离 +// 入参: page 页面索引, path 完整对象路径, value 新的富文本内容 +// 返回: error 路径、排版或提交错误 +func (e *Editor) ReflowRichText(page int, path ObjectPath, value RichText) error { + if len(path.Children) == 0 { + if path.Annotation != "" { + return ErrNotRichText + } + return e.updateRichTextFrame(page, path.ID, value, 0, 0, true) } index := path.Children[len(path.Children)-1] path.Children = path.Children[:len(path.Children)-1] - member, err := e.CompositeObject(page, path, index) - if err != nil { - return RichText{}, err - } - if member.Object.Type == "TextObject" { - text := member.Object.TextObject - text.CTM = "" - return e.promoteTextValue(text) - } - value, _, _, err := e.CompositeRichText(page, member) - return value, err + return e.updateCompositeRichText(page, path, index, value, 0, 0, true) }