// 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 ( "fmt" "image/color" "math" "reflect" "slices" "strings" "unicode/utf8" ) // TextSelection 指定同页同一直接容器的文字,Root为空使用IDs,否则使用Indexes // 分析返回的选区绑定文档和修订,结构或内容改变后需重新分析 type TextSelection struct { Root *ObjectPath IDs []string Indexes []int editor *Editor document editorDocumentIdentity revision uint64 page int } // TextSelectionEdit 指定码点范围替换,空Text表示删除,Font覆盖所有剩余文字 // Reflow非空时使用完整富文本显式重排并忽略范围和Text,Width为可选页面段落宽度,单位为毫米 // Size为页面毫米字号,零值保持原字号,Color为空保持原颜色,FontData优先于Font type TextSelectionEdit struct { Start, End int Text string Font string FontData []byte Size float64 Color string Reflow *RichText Width float64 } // TextSelectionPart 保存源图元及合并文本码点范围,Matrix将TextObject局部坐标映射至页面 // Deleted表示提交时删除源图元,重排生成项以Sources标识全部来源 // Points为不含换行的局部码点原点,Color为解析后的RGB分量,Start和End使用原合并文本范围 type TextSelectionPart struct { ID string Index int Start, End int TextObject TextObject Matrix Matrix Deleted bool Sources []int Points []Point Color string points []Point position ObjectPosition raw TextObject container *editorXML } // TextSelectionAnalysis 保存阅读顺序、格式和可执行能力,不插入推测空格 type TextSelectionAnalysis struct { Selection TextSelection Text string RichText RichText Parts []TextSelectionPart Bounds Box Replace, Delete, Reflow bool } // TextSelectionPreview 保存实际更新后的文字和源映射,删除项保留原范围 type TextSelectionPreview struct { Selection TextSelection Font string Text string Parts []TextSelectionPart Bounds Box analysis TextSelectionAnalysis } // AnalyzeTextSelection 分析同一容器内的横排文字,拒绝重叠及无法可靠映射的布局 // 入参: page 页面索引, selection 文字选区 // 返回: TextSelectionAnalysis 阅读顺序与能力, error 选择或布局错误 func (e *Editor) AnalyzeTextSelection(page int, selection TextSelection) (TextSelectionAnalysis, error) { if selection.editor != nil && (selection.editor != e || selection.page != page || !e.documentIdentity().matches(selection.document) || selection.revision != e.revision) { return TextSelectionAnalysis{}, fmt.Errorf("stale text selection") } copy := e.transactionSnapshot() result, err := copy.analyzeTextSelection(page, selection) if err == nil { result.Selection.editor, result.Selection.document, result.Selection.revision = e, e.documentIdentity(), e.revision result.Selection.page = page } return cloneEditorData(result), err } // analyzeTextSelection 在隔离资源视图中计算选区和逐字坐标 // 入参: page 页面索引, selection 文字选区 // 返回: TextSelectionAnalysis 分析快照, error 选择或布局错误 func (e *Editor) analyzeTextSelection(page int, selection TextSelection) (TextSelectionAnalysis, error) { result := TextSelectionAnalysis{Selection: selection, Replace: true, Delete: true, Reflow: true} result.Selection.IDs = slices.Clone(selection.IDs) result.Selection.Indexes = slices.Clone(selection.Indexes) if selection.Root != nil { root := cloneEditorData(*selection.Root) result.Selection.Root = &root if len(selection.IDs) != 0 || len(selection.Indexes) == 0 { return result, fmt.Errorf("composite selection requires indexes only") } members, err := e.CompositeObjectsAt(page, root, selection.Indexes) if err != nil { return result, err } reader, _, _, nodes, err := e.compositeScope(page, root) if err != nil { return result, err } defer reader.Close() for i, member := range members { if member.Object.Type != "TextObject" || !member.Capabilities.RewriteText { return result, fmt.Errorf("selection member is not editable text") } node := nodes[selection.Indexes[i]] result.Parts = append(result.Parts, TextSelectionPart{ID: member.Object.TextObject.ID, Index: selection.Indexes[i], TextObject: member.Object.TextObject, raw: node.object.TextObject, Matrix: member.Matrix, position: member.Position, container: node.span.parent}) } } else { if len(selection.Indexes) != 0 || len(selection.IDs) == 0 { return result, fmt.Errorf("page selection requires IDs only") } objects, _, err := e.selectedObjects(page, selection.IDs) if err != nil { return result, err } for _, object := range objects { if object.Type != "TextObject" { return result, fmt.Errorf("selection member is not text") } t := object.TextObject capability, err := e.ObjectCapabilities(page, t.ID) if err != nil { return result, err } if !capability.RewriteText { return result, capability.editError() } position, err := e.ObjectPosition(page, t.ID) if err != nil { return result, err } layer, _, err := e.findObject(page, t.ID) if err != nil { return result, err } resolved, err := e.resolveEditorStyle(object, layer.DrawParam) if err != nil { return result, err } box, err := ParseBox(t.Boundary) if err != nil { return result, err } container, _ := e.objectOrderIndexes(page, layer, t.ID) result.Parts = append(result.Parts, TextSelectionPart{ID: t.ID, Index: position.Index, TextObject: resolved.TextObject, raw: t, Matrix: TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(t.CTM)), position: position, container: container}) } } positions := make([]int, len(result.Parts)) for i, part := range result.Parts { positions[i] = part.position.Index } slices.Sort(positions) for i := 1; i < len(positions); i++ { if positions[i] != positions[i-1]+1 { return result, fmt.Errorf("text selection skips unselected container members") } } for i := range result.Parts { p := &result.Parts[i] if p.container != result.Parts[0].container || p.position.Container != result.Parts[0].position.Container || p.position.Layer != result.Parts[0].position.Layer { return result, fmt.Errorf("text selection crosses containers") } t := p.TextObject m := p.Matrix if math.Abs(m.a-m.d) > 1e-9*math.Max(m.a, m.d) { result.Reflow = false } if t.ReadDirection != 0 || t.CharDirection != 0 || t.VScale != 0 || t.Decoration != "" || math.Abs(m.b) > 1e-9 || math.Abs(m.c) > 1e-9 || m.a <= 0 || m.d <= 0 { return result, fmt.Errorf("text selection requires unambiguous horizontal character mapping") } if err := validateTextGlyphs(t, 1<<16); err != nil { return result, err } for _, transform := range t.CGTransform { if transform.CodeCount != transform.GlyphCount { return result, fmt.Errorf("combined glyphs require independent explicit layout") } } points, err := e.selectionTextPoints(t) if err != nil { return result, err } p.points = points chars := []rune(t.Text()) var previous Point hasPrevious := false for j, point := range points { if chars[j] == '\n' { continue } p.Points = append(p.Points, point) x, y := m.Transform(point.X, point.Y) if !finite(x) || !finite(y) || hasPrevious && (point.Y < previous.Y-1e-7 || point.Y > previous.Y+1e-7 && chars[j-1] != '\n' || math.Abs(point.Y-previous.Y) < 1e-7 && point.X <= previous.X) { return result, fmt.Errorf("ambiguous text reading order") } previous, hasPrevious = point, true result.Bounds = unionTextBox(result.Bounds, m.TransformBox(Box{X: point.X, Y: point.Y - t.Size, W: t.Size, H: t.Size})) } if t.Clips != nil || len(t.Actions) != 0 || t.DrawParam != "" || t.Alpha != nil && *t.Alpha != 255 || t.Visible != nil && !*t.Visible || t.Stroke != nil && *t.Stroke || t.Fill != nil && !*t.Fill || t.HScale != 0 && t.HScale != 1 || t.Weight != 0 && t.Weight != 400 && t.Weight != 700 { result.Reflow = false } } slices.SortFunc(result.Parts, func(a, b TextSelectionPart) int { ax, ay := a.Matrix.Transform(a.points[0].X, a.points[0].Y) bx, by := b.Matrix.Transform(b.points[0].X, b.points[0].Y) if ay < by { return -1 } if ay > by { return 1 } if ax < bx { return -1 } if ax > bx { return 1 } return 0 }) for start := 0; start < len(result.Parts); { first := result.Parts[start] _, anchor := first.Matrix.Transform(first.points[0].X, first.points[0].Y) end := start + 1 for end < len(result.Parts) { part := result.Parts[end] _, y := part.Matrix.Transform(part.points[0].X, part.points[0].Y) if y-anchor > 1e-6 { break } end++ } slices.SortStableFunc(result.Parts[start:end], func(a, b TextSelectionPart) int { ax, _ := a.Matrix.Transform(a.points[0].X, a.points[0].Y) bx, _ := b.Matrix.Transform(b.points[0].X, b.points[0].Y) if ax < bx { return -1 } if ax > bx { return 1 } return 0 }) start = end } var baseline, lastEnd, lastOrigin, previousSize, rowStart float64 var text strings.Builder offset := 0 for i := range result.Parts { p := &result.Parts[i] x, y := p.Matrix.Transform(p.points[0].X, p.points[0].Y) if i > 0 && (y < baseline-1e-6 || math.Abs(y-baseline) < 1e-6 && (x <= lastOrigin+1e-7 || x < lastEnd-1e-7)) { return result, fmt.Errorf("text objects overlap or interleave") } size := p.TextObject.Size * p.Matrix.d if i > 0 { limit := math.Max(size, previousSize) if math.Abs(y-baseline) < 1e-6 && x-lastEnd > limit*8 || y > baseline+1e-6 && (y-baseline > limit*3 || math.Abs(x-rowStart) > limit*2) { return result, fmt.Errorf("text selection has ambiguous column or paragraph gaps") } } if i == 0 || y > baseline+1e-6 { rowStart = x } if i > 0 && y > baseline+1e-6 { text.WriteByte('\n') offset++ result.RichText.Runs[len(result.RichText.Runs)-1].Text += "\n" } lastOrigin, baseline = p.Matrix.Transform(p.points[len(p.points)-1].X, p.points[len(p.points)-1].Y) font, err := e.editorFont(p.TextObject.Font) if err != nil { return result, err } chars := []rune(strings.ReplaceAll(p.TextObject.Text(), "\n", "")) glyph := font.GlyphIndex(chars[len(chars)-1]) for _, mapping := range p.TextObject.CGTransform { if len(chars)-1 >= mapping.CodePosition && len(chars)-1 < mapping.CodePosition+mapping.CodeCount { glyph = uint16(parseInts(mapping.Glyphs)[len(chars)-1-mapping.CodePosition]) } } scale := p.TextObject.HScale if scale == 0 { scale = 1 } lastEnd = lastOrigin + float64(font.GlyphAdvance(glyph))*p.TextObject.Size/float64(font.UnitsPerEm())*scale*p.Matrix.a previousSize = size value := p.TextObject.Text() p.Start = offset text.WriteString(value) offset += utf8.RuneCountInString(value) p.End = offset format := TextFormat{Font: p.TextObject.Font, Size: p.TextObject.Size * p.Matrix.d, Color: "0 0 0", Bold: p.TextObject.Weight == 700, Italic: p.TextObject.Italic} if p.TextObject.FillColor != nil { c := p.TextObject.FillColor if c.Pattern != nil || c.AxialShd != nil || c.RadialShd != nil || c.GouraudShd != nil || c.LaGouraudShd != nil || c.unsupported { result.Reflow = false } color, err := e.Color(p.TextObject.FillColor) if err != nil { return result, err } r, g, b, a := color.RGBA() if a > 0 && a < 65535 { r = r * 65535 / a g = g * 65535 / a b = b * 65535 / a } format.Color = fmt.Sprintf("%d %d %d", r>>8, g>>8, b>>8) if a != 65535 { result.Reflow = false } } result.RichText.Runs = append(result.RichText.Runs, RichTextRun{Text: value, Format: format}) p.Color = format.Color } result.Text = text.String() if result.Reflow && e.source != nil && (e.source.document.CustomTags.Path != "" || len(e.source.document.CustomTags.CustomTag) != 0) { structures, err := e.Structure() if err != nil { result.Reflow = false } else { ids := make(map[string]bool, len(result.Parts)+1) for _, part := range result.Parts { ids[editorResourceID(part.ID)] = true } if selection.Root != nil { ids[editorResourceID(selection.Root.ID)] = true for i, index := range selection.Root.Children { parent := *selection.Root parent.Children = parent.Children[:i] member, err := e.CompositeObject(page, parent, index) if err != nil { result.Reflow = false break } ids[editorResourceID(editorObjectID(member.Object))] = true } } pageID := editorResourceID(e.pages[page].ID) for _, structure := range structures { if structure.Error != "" || structure.Root == nil || !structure.CanBind { result.Reflow = false break } pending := []*StructureNode{structure.Root} for len(pending) != 0 && result.Reflow { node := pending[len(pending)-1] pending = pending[:len(pending)-1] for _, ref := range node.References { if editorResourceID(ref.PageID) != pageID { continue } for _, id := range ref.ObjectIDs { if ids[editorResourceID(id)] { result.Reflow = false } } } for i := range node.Children { pending = append(pending, &node.Children[i]) } } } } } return result, nil } // selectionTextPoints 读取标准逐字原点,不推断缺失的字符映射 // 入参: t 文字对象 // 返回: []Point 含换行占位的码点原点, error 字体或映射错误 func (e *Editor) selectionTextPoints(t TextObject) ([]Point, error) { font, err := e.editorFont(t.Font) if err != nil { return nil, err } ascent, _, _ := font.VerticalMetrics() var points []Point lines := newTextLineScanner(t) mappings := make(map[int]CGTransform, len(t.CGTransform)) for _, mapping := range t.CGTransform { mappings[mapping.CodePosition] = mapping } offset := 0 for _, code := range textCodeOrigins(t.TextCode, float64(ascent)*t.Size/float64(font.UnitsPerEm())) { if lines.next(code) { points = append(points, Point{}) } if code.Index != "" { return nil, fmt.Errorf("glyph-index text has no character mapping") } position := NewTextPositioner(code) chars := textCodeRunes(code.Value) for i := 0; i < len(chars); { mapping, ok := mappings[offset+i] if !ok { points = append(points, position.Next()) i++ continue } if mapping.CodeCount <= 0 || mapping.GlyphCount <= 0 || i+mapping.CodeCount > len(chars) { return nil, fmt.Errorf("invalid text character mapping") } glyphPoints := make([]Point, mapping.GlyphCount) for j := range glyphPoints { glyphPoints[j] = position.Next() } for j := 0; j < mapping.CodeCount; j++ { points = append(points, glyphPoints[min(j, len(glyphPoints)-1)]) } i += mapping.CodeCount } offset += len(chars) } if len(points) == 0 || len(points) != utf8.RuneCountInString(t.Text()) { return nil, fmt.Errorf("text selection requires a nonempty character mapping") } return points, nil } // PreviewTextSelection 在隔离快照生成与提交相同的结果,不分配文档资源或修改历史 // 入参: page 页面索引, selection 文字选区, edit 编辑范围与样式 // 返回: TextSelectionPreview 更新文字与来源映射, error 校验错误 func (e *Editor) PreviewTextSelection(page int, selection TextSelection, edit TextSelectionEdit) (TextSelectionPreview, error) { analysis, err := e.AnalyzeTextSelection(page, selection) if err != nil { return TextSelectionPreview{}, err } copy := e.transactionSnapshot() if err := copy.prepareTextSelectionEdit(&edit); err != nil { return TextSelectionPreview{}, err } result, err := copy.previewTextSelection(analysis, edit) return cloneEditorData(result), err } // previewTextSelection 统一生成定位更新或显式富文本布局 // 入参: analysis 选区快照, edit 编辑参数 // 返回: TextSelectionPreview 更新预览, error 编辑错误 func (e *Editor) previewTextSelection(analysis TextSelectionAnalysis, edit TextSelectionEdit) (TextSelectionPreview, error) { result := TextSelectionPreview{analysis: analysis, Bounds: analysis.Bounds, Font: edit.Font} if edit.Reflow != nil { if !analysis.Reflow { return result, fmt.Errorf("selection appearance cannot be safely reflowed") } value := cloneEditorData(*edit.Reflow) for i := range value.Runs { if edit.Font != "" { value.Runs[i].Format.Font = edit.Font } if edit.Size != 0 { value.Runs[i].Format.Size = edit.Size } if edit.Color != "" { value.Runs[i].Format.Color = edit.Color } } width := edit.Width if width == 0 { width = analysis.Bounds.W } layout, err := e.layoutRichText(value, width) if err != nil { return result, err } result.Text = value.Text() for _, object := range layout.objects { if object.Type != "TextObject" { continue } box, err := ParseBox(object.TextObject.Boundary) if err != nil { return result, err } part := TextSelectionPart{Index: -1, TextObject: object.TextObject, Matrix: TranslationMatrix(analysis.Bounds.X+box.X, analysis.Bounds.Y+box.Y).Multiply(NewMatrix(object.TextObject.CTM))} part.Color = "0 0 0" if object.TextObject.FillColor != nil { part.Color = object.TextObject.FillColor.Value } points, err := e.selectionTextPoints(object.TextObject) if err != nil { return result, err } for i, char := range []rune(object.TextObject.Text()) { if char != '\n' { part.Points = append(part.Points, points[i]) } } for i := range analysis.Parts { part.Sources = append(part.Sources, i) } result.Parts = append(result.Parts, part) } result.Selection = selectionAfterTextEdit(analysis, nil, true) return result, nil } if !utf8.ValidString(edit.Text) || strings.ContainsAny(edit.Text, "\r\t") || edit.Start < 0 || edit.End < edit.Start || edit.End > utf8.RuneCountInString(analysis.Text) { return result, fmt.Errorf("invalid text selection range") } boundaries := richTextBoundaries(analysis.Text) if !boundaries[edit.Start] || !boundaries[edit.End] { return result, fmt.Errorf("text range splits a grapheme") } replacement := []rune(edit.Text) originalText := []rune(analysis.Text) wantedText := string(originalText[:edit.Start]) + edit.Text + string(originalText[edit.End:]) if len(replacement) != 0 && len(replacement) != edit.End-edit.Start { return result, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("positioned replacement must have equal length or be a deletion")} } for _, source := range analysis.Parts { p := source t := cloneEditorData(source.TextObject) old := []rune(t.Text()) var next []rune var codes []TextCode var transforms []CGTransform glyphs := make(map[int]string) for _, mapping := range t.CGTransform { for j, glyph := range strings.Fields(mapping.Glyphs) { glyphs[mapping.CodePosition+j] = glyph } } glyphPosition, retained := 0, 0 p.Points = nil for j, char := range old { at := source.Start + j original := char if char != '\n' { glyphPosition++ } if at >= edit.Start && at < edit.End { if len(replacement) == 0 { continue } char = replacement[at-edit.Start] } if (original == '\n') != (char == '\n') { return result, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing line breaks requires reflow")} } next = append(next, char) if char == '\n' { continue } point := source.points[j] p.Points = append(p.Points, point) codes = append(codes, TextCode{X: ofdNumber(point.X), Y: ofdNumber(point.Y), Value: escapeOFDText(string(char))}) if glyph, ok := glyphs[glyphPosition-1]; ok { transforms = append(transforms, CGTransform{CodePosition: retained, CodeCount: 1, GlyphCount: 1, Glyphs: glyph}) } retained++ } p.Deleted = len(codes) == 0 if !p.Deleted { if len(next) == len(old) && (string(next) != string(old) || edit.Font != "" && edit.Font != t.Font) { if err := e.RewriteTextWithFont(&t, string(next), edit.Font); err != nil { return result, err } } else if len(next) != len(old) { t.TextCode, t.CGTransform, t.layout = codes, transforms, nil if err := e.RewriteTextWithFont(&t, t.Text(), edit.Font); err != nil { return result, err } } if edit.Size != 0 { t.Size = edit.Size / source.Matrix.d } if edit.Color != "" { p.Color = edit.Color var r, g, b float64 fmt.Sscan(edit.Color, &r, &g, &b) fill, err := e.RGBColor(color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255}) if err != nil { return result, err } t.FillColor = editorStyleColor(fill, t.FillColor) } if err := validateEditorGeometry(GraphicObject{Type: "TextObject", TextObject: t}); err != nil { return result, err } if result.Text != "" && source.Start > 0 && []rune(analysis.Text)[source.Start-1] == '\n' { result.Text += "\n" } result.Text += t.Text() } p.TextObject = t result.Parts = append(result.Parts, p) } result.Selection = selectionAfterTextEdit(analysis, result.Parts, false) if result.Text != wantedText { return result, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing positioned line breaks requires explicit reflow")} } return result, nil } // prepareTextSelectionEdit 在当前隔离视图校验覆盖样式并注册替换字体 // 入参: edit 编辑参数 // 返回: error 字体或样式错误 func (e *Editor) prepareTextSelectionEdit(edit *TextSelectionEdit) error { if !finite(edit.Size) || edit.Size < 0 || !finite(edit.Width) || edit.Width < 0 { return fmt.Errorf("invalid text selection dimensions") } if edit.Color != "" { if err := creationColor(&FillColor{Value: edit.Color}); err != nil { return err } } if len(edit.FontData) > 0 { id, err := e.AddFont(FontFile{Data: edit.FontData}, 0) if err != nil { return err } edit.Font = id } return nil } // selectionAfterTextEdit 计算删除及重排后的选中位置,不保留旧修订绑定 // 入参: analysis 原选区, parts 更新项, reflow 是否合并为富文本 // 返回: TextSelection 提交后可用于重新枚举的选择 func selectionAfterTextEdit(analysis TextSelectionAnalysis, parts []TextSelectionPart, reflow bool) TextSelection { result := TextSelection{Root: analysis.Selection.Root} deleted := make(map[int]bool) if reflow { for _, part := range analysis.Parts[1:] { deleted[part.Index] = true } parts = analysis.Parts[:1] } else { for _, part := range parts { if part.Deleted { deleted[part.Index] = true } } } for _, part := range parts { if part.Deleted { continue } if result.Root == nil { result.IDs = append(result.IDs, part.ID) continue } index := part.Index for removed := range deleted { if removed < part.Index { index-- } } result.Indexes = append(result.Indexes, index) } return result } // UpdateTextSelection 原子提交联合文字修改,失败不改变资源、ID或撤销重做 // 入参: page 页面索引, selection 文字选区, edit 编辑范围与样式 // 返回: error 校验或提交错误 func (e *Editor) UpdateTextSelection(page int, selection TextSelection, edit TextSelectionEdit) error { analysis, err := e.AnalyzeTextSelection(page, selection) if err != nil { return err } return e.Transaction(func(e *Editor) error { if err := e.prepareTextSelectionEdit(&edit); err != nil { return err } preview, err := e.previewTextSelection(analysis, edit) if err != nil { return err } if edit.Reflow != nil { return e.reflowTextSelection(page, selection, edit, preview) } if selection.Root != nil { return e.editCompositeObjects(page, *selection.Root, selection.Indexes, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error { for i, node := range nodes { for _, part := range preview.Parts { if part.Index != selection.Indexes[i] { continue } if part.Deleted { node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, nil}) continue } object := cloneEditorData(node.object) object.TextObject.TextCode, object.TextObject.CGTransform = part.TextObject.TextCode, part.TextObject.CGTransform if edit.Font != "" { object.TextObject.Font = part.TextObject.Font } if edit.Size != 0 { object.TextObject.Size = part.TextObject.Size } if edit.Color != "" { object.TextObject.FillColor = part.TextObject.FillColor } if reflect.DeepEqual(object, node.object) { continue } if err := node.update(object); err != nil { return err } node.setState(editorCompositeState{}) } } return nil }) } var updates []GraphicObject var deleted []string for _, part := range preview.Parts { if part.Deleted { deleted = append(deleted, part.ID) continue } next := selectionUpdatedText(part, edit) if !reflect.DeepEqual(next, part.raw) { updates = append(updates, GraphicObject{Type: "TextObject", TextObject: next}) } } if len(updates) > 0 { if err := e.updateObjects(page, updates, true); err != nil { return err } } if len(deleted) > 0 { return e.DeleteObjects(page, deleted) } return nil }) } // selectionUpdatedText 仅更新请求字段,保留源对象继承样式与排版状态 // 入参: part 预览项, edit 编辑参数 // 返回: TextObject 可提交的原始对象 func selectionUpdatedText(part TextSelectionPart, edit TextSelectionEdit) TextObject { next := cloneEditorData(part.raw) next.TextCode, next.CGTransform, next.layout = part.TextObject.TextCode, part.TextObject.CGTransform, part.TextObject.layout if edit.Font != "" { next.Font = part.TextObject.Font } if edit.Size != 0 { next.Size = part.TextObject.Size } if edit.Color != "" { next.FillColor = part.TextObject.FillColor } return next } // reflowTextSelection 将富文本排版结果替换至首个源位置,其他源成员一并删除 // 入参: page 页面索引, selection 原选择, edit 编辑参数, preview 布局预览 // 返回: error 排版或提交错误 func (e *Editor) reflowTextSelection(page int, selection TextSelection, edit TextSelectionEdit, preview TextSelectionPreview) error { value := cloneEditorData(*edit.Reflow) for i := range value.Runs { if edit.Font != "" { value.Runs[i].Format.Font = edit.Font } if edit.Size != 0 { value.Runs[i].Format.Size = edit.Size } if edit.Color != "" { value.Runs[i].Format.Color = edit.Color } } box := preview.Bounds if edit.Width != 0 { box.W = edit.Width } object, meta, err := e.prepareRichText(box, value) if err != nil { return err } first := preview.analysis.Parts[0] if selection.Root == nil { object.CompositeGraphicUnit.ID = first.ID if err := e.updateObjects(page, []GraphicObject{object}, true); err != nil { return err } var deleted []string for _, part := range preview.analysis.Parts[1:] { deleted = append(deleted, part.ID) } if len(deleted) > 0 { if err := e.DeleteObjects(page, deleted); err != nil { return err } } return e.putRichTextMetadata(page, first.ID, meta, -1) } return e.editCompositeObjects(page, *selection.Root, selection.Indexes, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error { for i, node := range nodes { if selection.Indexes[i] != first.Index { node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, nil}) continue } inverse, ok := node.parent.Invert() if !ok { return fmt.Errorf("invalid composite parent transform") } local := transformEditorMatrix(object, inverse) local.CompositeGraphicUnit.ID = node.object.TextObject.ID if !node.boundaryInCTM { local = compositeBoundary(local, node.parent, false) } if err := node.update(local); err != nil { return err } node.setState(editorCompositeState{}) } return nil }) }