Files
ofdgo/ofdgo_editor_rich_text.go
T
2026-10-28 10:06:12 +08:00

1255 lines
45 KiB
Go

// 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"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"image/color"
"io"
"reflect"
"slices"
"strconv"
)
// richTextTagType 标识富文本编辑标引,正文使用标准复合资源和图元
const richTextTagType = "OFDGo.RichText"
// richTextNamespace 标识富文本编辑语义的命名空间,版本仅随不兼容的语义变更升级
const richTextNamespace = "https://ofdgo.aite.me/ns/rich-text/1"
// richTextMetadata 不保存字体编号,以标准主文字对象序号恢复当前文档字体
type richTextMetadata struct {
Version int `json:"version"`
Value RichText `json:"value"`
Frame Box `json:"frame"`
Extent Box `json:"extent"`
Offset [2]float64 `json:"offset"`
Anchors []int `json:"anchors"`
Digest string `json:"digest"`
}
// richTextSnapshot 保存对象及嵌套成员的不可变编辑语义,成员按绘制顺序定位
type richTextSnapshot struct {
metadata *richTextMetadata
children map[int]*richTextSnapshot
}
// richTextEntry 保存标引入口及延迟报告的损坏诊断
type richTextEntry struct {
index int
metadata richTextMetadata
err error
}
// richTextIndex 缓存当前文档修订的标引,不创建页面预览快照
type richTextIndex struct {
source *editorSource
revision uint64
remaps map[string]int
entries map[string]richTextEntry
err error
}
// AddRichText 原子添加标准组合及持久化编辑标引,成功时只产生一条历史
// 入参: page 页面索引, box 页面编辑框, value 富文本内容
// 返回: string 组合对象标识, error 排版、资源或持久化错误
func (e *Editor) AddRichText(page int, box Box, value RichText) (string, error) {
var id string
err := e.Transaction(func(e *Editor) error {
if _, err := e.page(page); err != nil {
return err
}
if _, err := creationBox(editorBoxString(box)); err != nil {
return err
}
object, meta, err := e.prepareRichText(box, value)
if err != nil {
return err
}
id, err = e.AddObject(page, object)
if err != nil {
return err
}
return e.putRichTextMetadata(page, id, meta, -1)
})
if err != nil {
return "", err
}
return id, nil
}
// UpdateRichText 原子替换富文本内容,保留对象标识、局部编辑框及页面变换
// 安全普通文字显式重排后提升为组合;不修改共享的旧组合资源
// 入参: page 页面索引, id 对象标识, value 完整富文本
// 返回: error 识别、排版或提交错误
func (e *Editor) UpdateRichText(page int, id string, value RichText) error {
return e.updateRichTextFrame(page, id, value, 0, 0, false)
}
// ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放
// 入参: page 页面索引, id 对象标识, offset 局部横向偏移, width 新的局部段落宽度
// 返回: error 尺寸、排版或提交错误
func (e *Editor) ResizeRichTextFrame(page int, id string, offset, width float64) error {
if !finite(offset) || !finite(width) || width <= 0 {
return fmt.Errorf("rich text width must be positive and finite")
}
value, err := e.RichText(page, id)
if err != nil {
return err
}
return e.updateRichTextFrame(page, id, value, offset, width, false)
}
// updateRichTextFrame 统一提交正文或编辑框变化,width为零时沿用原宽度模式
// 入参: page 页面索引, id 对象标识, value 新内容, offset 局部偏移, width 可选宽度, reflow 是否显式重排原文
// 返回: 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, frame, matrix, err := e.PrepareRichText(page, ObjectPath{ID: id}, reflow)
if err != nil {
return err
}
if err := value.Validate(); err != nil {
return err
}
if width > 0 {
value.AutoWidth = false
}
if old.Type == "CompositeObject" && offset == 0 && (width == 0 || width == frame.W) && reflect.DeepEqual(normalizeRichText(current), normalizeRichText(value)) {
return nil
}
if width > 0 {
frame.W = width
matrix = matrix.Multiply(TranslationMatrix(offset, 0))
}
object, meta, err := e.prepareRichText(Box{W: frame.W, H: frame.H}, value)
if err != nil {
return err
}
object.CompositeGraphicUnit.ID = id
placement := richTextPlacement(meta, matrix)
object.CompositeGraphicUnit.Boundary = placement.Boundary
object.CompositeGraphicUnit.CTM = placement.CTM
if old.Type == "CompositeObject" {
unit := old.CompositeGraphicUnit
if unit.Clips != nil {
return fmt.Errorf("clipped rich text requires explicit clip removal before reflow")
}
unit.ResourceID = object.CompositeGraphicUnit.ResourceID
unit.Boundary = placement.Boundary
unit.CTM = placement.CTM
object.CompositeGraphicUnit = unit
}
index := -1
if old.Type == "CompositeObject" {
entry, err := e.richTextEntry(page, id)
if err != nil {
return err
}
index = entry.index
}
if err := e.updateObjects(page, []GraphicObject{object}, true); err != nil {
return err
}
return e.putRichTextMetadata(page, id, meta, index)
})
}
// DeleteRichText 原子删除富文本及其标引,不删除其他实例引用的共享资源
// 入参: page 页面索引, id 对象标识
// 返回: error 识别或删除错误
func (e *Editor) DeleteRichText(page int, id string) error {
return e.Transaction(func(e *Editor) error {
object, err := e.Object(page, id)
if err != nil {
return err
}
if _, err = e.RichText(page, id); err != nil {
return err
}
if object.Type == "CompositeObject" {
entry, err := e.richTextEntry(page, id)
if err != nil {
return err
}
if err := e.DeleteStructure(entry.index); err != nil {
return err
}
}
return e.DeleteObject(page, id)
})
}
// RichText 返回独立的富文本编辑值,未标引组合返回ErrNotRichText,损坏标引返回诊断
// 普通文字仅在定位和外观可以安全提升时返回单片段,否则明确拒绝
// 入参: page 页面索引, id 对象标识
// 返回: RichText 编辑内容, error 识别或持久化错误
func (e *Editor) RichText(page int, id string) (RichText, error) {
object, err := e.Object(page, id)
if err != nil {
return RichText{}, err
}
return e.richTextObject(page, object)
}
// richTextObject 读取对象的编辑标引并校验标准内容
// 入参: page 页面索引, object 当前对象
// 返回: RichText 编辑内容, error 识别或持久化错误
func (e *Editor) richTextObject(page int, object GraphicObject) (RichText, error) {
if object.Type == "TextObject" {
return e.promoteRichText(page, object)
}
if object.Type != "CompositeObject" {
return RichText{}, ErrNotRichText
}
entry, err := e.richTextEntry(page, editorObjectID(object))
if err != nil {
return RichText{}, err
}
unit, err := e.compositeDefinition(object.CompositeGraphicUnit.ResourceID)
if err != nil {
return RichText{}, fmt.Errorf("rich text resource: %w", err)
}
objects := unit.object.CompositeGraphicUnit.Objects
if richTextDigest(objects) != entry.metadata.Digest {
return RichText{}, fmt.Errorf("rich text content no longer matches its editing metadata")
}
value := cloneEditorData(entry.metadata.Value)
if len(entry.metadata.Anchors) != len(value.Runs) {
return RichText{}, fmt.Errorf("invalid rich text font anchors")
}
for i, anchor := range entry.metadata.Anchors {
if anchor < 0 || anchor >= len(objects) || objects[anchor].Type != "TextObject" {
return RichText{}, fmt.Errorf("invalid rich text font anchor %d", i)
}
value.Runs[i].Format.Font = objects[anchor].TextObject.Font
}
if err := value.Validate(); err != nil {
return RichText{}, fmt.Errorf("invalid rich text metadata: %w", err)
}
return value, nil
}
// RichTextFrame 返回原始局部编辑框及局部到页面的完整变换,不按组合缩放重新解释宽度
// 入参: page 页面索引, id 富文本或安全普通文字对象标识
// 返回: Box 局部编辑框, Matrix 页面变换, error 识别或几何错误
func (e *Editor) RichTextFrame(page int, id string) (Box, Matrix, error) {
object, err := e.Object(page, id)
if err != nil {
return Box{}, Matrix{}, err
}
if object.Type == "TextObject" {
_, frame, matrix, err := e.PrepareRichText(page, ObjectPath{ID: id}, false)
return frame, matrix, err
}
if object.Type != "CompositeObject" {
return Box{}, Matrix{}, ErrNotRichText
}
entry, err := e.richTextEntry(page, id)
if err != nil {
return Box{}, Matrix{}, err
}
box, err := creationBox(object.CompositeGraphicUnit.Boundary)
if err != nil {
return Box{}, Matrix{}, err
}
return entry.metadata.Frame, TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.CompositeGraphicUnit.CTM)).Multiply(TranslationMatrix(entry.metadata.Offset[0], entry.metadata.Offset[1])), nil
}
// AddCompositeRichText 向内部范围添加富文本,使用页面坐标并保存编辑标引
// 入参: page 页面索引, path 父路径, box 页面编辑框, value 富文本
// 返回: int 新成员序号, error 排版或提交错误
func (e *Editor) AddCompositeRichText(page int, path ObjectPath, box Box, value RichText) (int, error) {
var index int
err := e.Transaction(func(e *Editor) error {
if _, err := creationBox(editorBoxString(box)); err != nil {
return err
}
object, meta, err := e.prepareRichText(box, value)
if err != nil {
return err
}
index, err = e.AddCompositeObject(page, path, object)
if err != nil {
return err
}
member, err := e.CompositeObject(page, path, index)
if err != nil {
return err
}
return e.putCompositeRichTextMetadata(page, member.Object, meta, -1)
})
return index, err
}
// CompositeRichText 读取内部富文本值及局部编辑框,不提升未标引的普通文字
// 入参: page 页面索引, member 当前内部成员快照
// 返回: RichText 编辑内容, Box 局部编辑框, Matrix 页面变换, error 识别或持久化错误
func (e *Editor) CompositeRichText(page int, member CompositeMember) (RichText, Box, Matrix, error) {
if _, err := e.page(page); err != nil {
return RichText{}, Box{}, Matrix{}, err
}
if member.Object.Type != "CompositeObject" {
return RichText{}, Box{}, Matrix{}, ErrNotRichText
}
value, err := e.richTextObject(page, member.Object)
if err != nil {
return RichText{}, Box{}, Matrix{}, err
}
entry, err := e.richTextEntry(page, editorObjectID(member.Object))
if err != nil {
return RichText{}, Box{}, Matrix{}, err
}
return value, entry.metadata.Frame, member.Matrix.Multiply(TranslationMatrix(entry.metadata.Offset[0], entry.metadata.Offset[1])), nil
}
// UpdateCompositeRichText 原子重排内部富文本,保留父变换并隔离共享实例
// 入参: 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")
}
if width > 0 {
value.AutoWidth = false
}
return e.Transaction(func(e *Editor) error {
member, err := e.CompositeObject(page, path, index)
if err != nil {
return err
}
if member.Object.Type == "TextObject" {
return e.updateCompositePlainRichText(page, path, index, value, offset, width, reflow)
}
var metadata richTextMetadata
entryIndex := -1
changed := false
err = e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
node := nodes[0]
current, frame, _, err := e.CompositeRichText(page, members[0])
if err != nil {
return fmt.Errorf("read composite member %s: %w", editorObjectID(members[0].Object), err)
}
if err := value.Validate(); err != nil {
return err
}
if offset == 0 && (width == 0 || width == frame.W) && reflect.DeepEqual(normalizeRichText(current), normalizeRichText(value)) {
return nil
}
if node.object.CompositeGraphicUnit.Clips != nil {
return fmt.Errorf("clipped rich text requires explicit clip removal before reflow")
}
entry, err := e.richTextEntry(page, editorObjectID(node.object))
if err != nil {
return fmt.Errorf("read composite node %s: %w", editorObjectID(node.object), err)
}
object := cloneEditorData(node.object)
if !node.boundaryInCTM {
object = compositeBoundary(object, node.parent, true)
}
unit := &object.CompositeGraphicUnit
box, err := creationBox(unit.Boundary)
if err != nil {
return err
}
matrix := TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(unit.CTM)).Multiply(TranslationMatrix(entry.metadata.Offset[0]+offset, entry.metadata.Offset[1]))
if width > 0 {
frame.W = width
}
next, meta, err := e.prepareRichText(frame, value)
if err != nil {
return err
}
placement := richTextPlacement(meta, matrix)
unit.ResourceID, unit.Boundary, unit.CTM = next.CompositeGraphicUnit.ResourceID, placement.Boundary, placement.CTM
if !node.boundaryInCTM {
object = compositeBoundary(object, node.parent, false)
}
metadata, entryIndex, changed = meta, entry.index, true
return node.update(object)
})
if err != nil || !changed {
return err
}
member, err = e.CompositeObject(page, path, index)
if err != nil {
return err
}
if entry, err := e.richTextEntry(page, editorObjectID(member.Object)); err == nil {
entryIndex = entry.index
} else if !errors.Is(err, ErrNotRichText) {
return err
}
return e.putCompositeRichTextMetadata(page, member.Object, metadata, entryIndex)
})
}
// putCompositeRichTextMetadata 在容器坐标规范化后更新标准内容摘要和编辑标引
// 入参: page 页面索引, object 当前成员, meta 编辑信息, index 原入口或-1
// 返回: error 资源或提交错误
func (e *Editor) putCompositeRichTextMetadata(page int, object GraphicObject, meta richTextMetadata, index int) error {
unit, err := e.compositeDefinition(object.CompositeGraphicUnit.ResourceID)
if err != nil {
return err
}
meta.Digest = richTextDigest(unit.object.CompositeGraphicUnit.Objects)
return e.putRichTextMetadata(page, editorObjectID(object), meta, index)
}
// putCompositeRichTextCopies 一次读取复制后的选区,按对象顺序恢复富文本标引
// 入参: page 页面索引, path 父路径, indexes 新成员序号, metadata 选区位置对应的编辑语义
// 返回: error 读取或提交错误
func (e *Editor) putCompositeRichTextCopies(page int, path ObjectPath, indexes []int, metadata map[int]*richTextSnapshot) error {
if len(metadata) == 0 {
return nil
}
selected := make([]int, 0, len(metadata))
values := make([]*richTextSnapshot, 0, len(metadata))
for i, index := range indexes {
if meta, ok := metadata[i]; ok {
selected = append(selected, index)
values = append(values, meta)
}
}
members, err := e.CompositeObjectsAt(page, path, selected)
if err != nil {
return err
}
for i, member := range members {
if err := e.putRichTextSnapshot(page, member.Object, values[i]); err != nil {
return err
}
}
return nil
}
// prepareRichText 构造独立组合资源和不含字体编号的编辑标引
// 入参: box 页面或局部编辑框, value 富文本
// 返回: GraphicObject 标准组合, richTextMetadata 标引, error 准备错误
func (e *Editor) prepareRichText(box Box, value RichText) (GraphicObject, richTextMetadata, error) {
if err := value.Validate(); err != nil {
return GraphicObject{}, richTextMetadata{}, err
}
if box.W <= 0 || box.H <= 0 {
return GraphicObject{}, richTextMetadata{}, fmt.Errorf("rich text frame must be positive")
}
result, err := e.layoutRichText(value, box.W)
if err != nil {
return GraphicObject{}, richTextMetadata{}, err
}
box.W = result.width
if value.AutoWidth && !value.Layout.Wrap {
box.H = result.bounds.Y + result.bounds.H
}
padX, padY := -min(0, result.bounds.X), -min(0, result.bounds.Y)
extent := Box{W: max(box.W, result.bounds.X+result.bounds.W) + padX, H: max(box.H, result.bounds.Y+result.bounds.H) + padY}
if !finite(extent.W) || !finite(extent.H) || extent.W <= 0 || extent.H <= 0 {
return GraphicObject{}, richTextMetadata{}, fmt.Errorf("rich text extent exceeds finite range")
}
if err := e.prepareSourceIDs(); err != nil {
return GraphicObject{}, richTextMetadata{}, err
}
for i := range result.objects {
if padX != 0 || padY != 0 {
result.objects[i] = transformEditorMatrix(result.objects[i], TranslationMatrix(padX, padY))
}
if err := e.richTextColors(&result.objects[i]); err != nil {
return GraphicObject{}, richTextMetadata{}, err
}
}
resource, err := e.addOwnedVector(context.Background(), result.objects, extent.W, extent.H)
if err != nil {
return GraphicObject{}, richTextMetadata{}, err
}
unit, err := e.compositeDefinition(resource)
if err != nil {
return GraphicObject{}, richTextMetadata{}, err
}
meta := richTextMetadata{Version: 1, Value: cloneEditorData(value), Frame: Box{W: box.W, H: max(box.H, result.bounds.Y+result.bounds.H)}, Extent: extent, Offset: [2]float64{padX, padY}, Anchors: result.anchors, Digest: richTextDigest(unit.object.CompositeGraphicUnit.Objects)}
for i := range meta.Value.Runs {
meta.Value.Runs[i].Format.Font = ""
}
object := GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: richTextPlacement(meta, TranslationMatrix(box.X, box.Y))}
object.CompositeGraphicUnit.ResourceID = resource
return object, meta, nil
}
// richTextPlacement 根据局部逻辑坐标确定物理组合边界,负向装饰不会被资源裁剪
// 入参: meta 编辑框与资源尺寸, matrix 逻辑坐标到页面变换
// 返回: CompositeGraphicUnit 组合几何
func richTextPlacement(meta richTextMetadata, matrix Matrix) CompositeGraphicUnit {
matrix = matrix.Multiply(TranslationMatrix(-meta.Offset[0], -meta.Offset[1]))
box := matrix.TransformBox(meta.Extent)
return CompositeGraphicUnit{Boundary: editorBoxString(box), CTM: TranslationMatrix(-box.X, -box.Y).Multiply(matrix).String()}
}
// richTextDigest 对已编码图元计算不含字体、颜色空间和字形编号的内容摘要
// 入参: objects 标准子对象
// 返回: string 稳定内容摘要
func richTextDigest(objects []GraphicObject) string {
copy := cloneEditorData(objects)
for i := range copy {
o := &copy[i]
o.origin = nil
o.state = editorCompositeState{}
o.TextObject.ID = ""
o.TextObject.Font = ""
o.TextObject.CGTransform = nil
o.TextObject.layout = nil
o.PathObject.ID = ""
if o.TextObject.FillColor != nil {
o.TextObject.FillColor.ColorSpace = ""
}
if o.TextObject.StrokeColor != nil {
o.TextObject.StrokeColor.ColorSpace = ""
}
if o.PathObject.FillColor != nil {
o.PathObject.FillColor.ColorSpace = ""
}
if o.PathObject.StrokeColor != nil {
o.PathObject.StrokeColor.ColorSpace = ""
}
}
data, _ := json.Marshal(copy)
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}
// richTextEntry 从当前文档标引缓存定位对象,损坏项不降级为普通组合
// 入参: page 页面索引, id 组合标识
// 返回: richTextEntry 标引, error 缺失或损坏错误
func (e *Editor) richTextEntry(page int, id string) (richTextEntry, error) {
index := e.richTextMetadataIndex()
if index.err != nil {
return richTextEntry{}, index.err
}
entry, ok := index.entries[editorResourceID(e.pages[page].ID)+"/"+editorResourceID(id)]
if !ok {
return richTextEntry{}, ErrNotRichText
}
if entry.err != nil {
return entry, entry.err
}
return entry, nil
}
// richTextMetadataIndex 按来源和修订缓存标引,仅读取标引文件,不生成Reader快照
// 返回: *richTextIndex 只读缓存
func (e *Editor) richTextMetadataIndex() *richTextIndex {
if index := e.richIndex; index != nil && index.source == e.source && index.revision == e.revision {
valid := len(index.remaps) == len(e.structureRemaps)
for page, remaps := range e.structureRemaps {
valid = valid && index.remaps[page] == len(remaps)
}
if valid {
return index
}
}
index := &richTextIndex{source: e.source, revision: e.revision, remaps: e.structureRemapStarts(), entries: make(map[string]richTextEntry)}
e.richIndex = index
if e.source == nil {
return index
}
tags, err := e.source.reader.CustomTags()
if err != nil {
index.err = err
return index
}
for i, tag := range tags {
if tag.TypeID != richTextTagType || tag.NameSpace != richTextNamespace {
continue
}
structure, err := e.source.reader.readStructureContext(context.Background(), tag)
if err != nil {
index.err = fmt.Errorf("rich text metadata: %w", err)
return index
}
root := structure.Root
entry := richTextEntry{index: i}
if root.Name != "RichText" || root.Namespace != richTextNamespace {
entry.err = fmt.Errorf("invalid rich text metadata root")
} else {
entry.err = json.Unmarshal([]byte(root.Text), &entry.metadata)
}
if entry.err == nil && (entry.metadata.Version != 1 || entry.metadata.Frame.X != 0 || entry.metadata.Frame.Y != 0 || !finite(entry.metadata.Frame.W) || !finite(entry.metadata.Frame.H) || entry.metadata.Frame.W <= 0 || entry.metadata.Frame.H <= 0 || len(entry.metadata.Digest) != 64) {
entry.err = fmt.Errorf("invalid rich text metadata version or frame")
}
if entry.err == nil {
meta := entry.metadata
if meta.Extent.X != 0 || meta.Extent.Y != 0 || !finite(meta.Extent.W) || !finite(meta.Extent.H) || meta.Extent.W <= 0 || meta.Extent.H <= 0 || !finite(meta.Offset[0]) || !finite(meta.Offset[1]) || meta.Offset[0] < 0 || meta.Offset[1] < 0 || meta.Offset[0] > meta.Extent.W || meta.Offset[1] > meta.Extent.H || len(meta.Anchors) != len(meta.Value.Runs) {
entry.err = fmt.Errorf("invalid rich text metadata extent or anchors")
}
}
for _, child := range root.Children {
for _, ref := range child.References {
for _, id := range ref.ObjectIDs {
key := editorResourceID(ref.PageID) + "/" + editorResourceID(id)
if _, exists := index.entries[key]; exists {
entry.err = fmt.Errorf("ambiguous rich text metadata")
}
index.entries[key] = entry
}
}
}
}
for page, remaps := range e.structureRemaps {
for _, ids := range remaps {
for from, to := range ids {
if _, exists := index.entries[page+"/"+to]; !exists {
if entry, ok := index.entries[page+"/"+from]; ok {
index.entries[page+"/"+to] = entry
}
}
}
}
}
return index
}
// putRichTextMetadata 添加或替换带有标准标引引用的富文本语义
// 入参: page 页面索引, id 对象标识, meta 编辑信息, index 原入口或-1
// 返回: error 编码或提交错误
func (e *Editor) putRichTextMetadata(page int, id string, meta richTextMetadata, index int) error {
data, err := json.Marshal(meta)
if err != nil {
return err
}
root := StructureNode{Name: "RichText", Namespace: richTextNamespace, Text: string(data)}
if index >= 0 {
separate := false
err := e.editStructure(index, func(data []byte, node *editorXML, tag CustomTag) ([]byte, error) {
updated, err := rewriteStructureReferences(context.Background(), data, tag, func(pageID string, ids []string) (string, []string, error) {
var remaining []string
for _, objectID := range ids {
if editorResourceID(pageID) != editorResourceID(e.pages[page].ID) || editorResourceID(objectID) != editorResourceID(id) {
remaining = append(remaining, objectID)
separate = true
}
}
return pageID, remaining, nil
})
if err != nil || separate {
return updated, err
}
return updateStructureNodeXML(data, node, root)
})
if err != nil || !separate {
return err
}
}
root.Children = []StructureNode{{Name: "ObjectRef", Namespace: richTextNamespace, Attributes: []StructureAttribute{{Name: "PageRef", Value: e.pages[page].ID}}, Text: id}}
_, err = e.AddStructure(CustomTag{TypeID: richTextTagType, NameSpace: richTextNamespace}, root)
return err
}
// promoteRichText 仅提升已有明确排版或可验证单行定位的普通文字
// 入参: page 页面索引, object 普通文字快照
// 返回: RichText 单片段, error 不安全提升错误
func (e *Editor) promoteRichText(page int, object GraphicObject) (RichText, error) {
layer, _, err := e.findObject(page, object.TextObject.ID)
if err != nil {
return RichText{}, err
}
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)
}
// promoteTextValue 校验普通文字是否可无损提升,保留明确段落或可验证单行布局
// 入参: t 有效文字样式与局部定位
// 返回: RichText 单片段, error 布局或格式错误
func (e *Editor) promoteTextValue(t TextObject) (RichText, error) {
unsupported := func() (RichText, error) {
return RichText{}, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("text cannot be safely promoted to rich text")}
}
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()
}
if _, err := t.TextFrame(); err != nil {
return unsupported()
}
value, layout := t.TextLayout()
if t.layout == nil {
if len(t.TextCode) != 1 || len(t.CGTransform) != 0 {
return unsupported()
}
check := t
if err := e.LayoutText(&check, value, TextLayout{}); err != nil {
return RichText{}, err
}
actual, expected := t.TextCode[0], check.TextCode[0]
if actual.Index != "" || len(textCodeRunes(actual.Value)) != len(textCodeRunes(expected.Value)) {
return unsupported()
}
if !reflect.DeepEqual(parseFloatsWithG(actual.DeltaX), parseFloatsWithG(expected.DeltaX)) {
return unsupported()
}
for _, dy := range parseFloatsWithG(actual.DeltaY) {
if dy != 0 {
return unsupported()
}
}
if _, _, err := e.richPlainOffset(t); err != nil {
return unsupported()
}
}
color := "0 0 0"
if t.FillColor != nil {
c := t.FillColor
if c.Pattern != nil || c.AxialShd != nil || c.RadialShd != nil || c.GouraudShd != nil || c.LaGouraudShd != nil || c.Alpha != nil && *c.Alpha != 255 || c.unsupported {
return unsupported()
}
resolved, err := e.Color(c)
if err != nil {
return RichText{}, err
}
r, g, b, a := resolved.RGBA()
if a != 65535 {
return unsupported()
}
color = fmt.Sprintf("%d %d %d", r>>8, g>>8, b>>8)
}
result := RichText{AutoWidth: t.layout == nil, Runs: []RichTextRun{{Text: value, Format: TextFormat{Font: t.Font, Size: t.Size, Color: color, Bold: t.Weight == 700, Italic: t.Italic}}}, Layout: layout}
return result, result.Validate()
}
// richPlainOffset 计算原首行相对标准排版的位移,保留既有段落和组合内基线
// 入参: text 已校验的普通文字
// 返回: float64 横向偏移, float64 基线偏移, error 无效坐标
func (e *Editor) richPlainOffset(text TextObject) (float64, float64, error) {
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, value, layout); err != nil {
return 0, 0, err
}
x, err := strconv.ParseFloat(text.TextCode[0].X, 64)
if err != nil || !finite(x) {
return 0, 0, fmt.Errorf("invalid text origin")
}
y, err := strconv.ParseFloat(text.TextCode[0].Y, 64)
if err != nil || !finite(y) {
return 0, 0, fmt.Errorf("invalid text baseline")
}
baseline, err := strconv.ParseFloat(check.TextCode[0].Y, 64)
if err != nil {
return 0, 0, err
}
origin, err := strconv.ParseFloat(check.TextCode[0].X, 64)
if err != nil {
return 0, 0, err
}
return x - origin, y - baseline, nil
}
// copyRichTextMetadata 迁移选区及其组合、注解内部的编辑标引,复用标准内容中的字体引用
// 入参: ctx 取消上下文, source 来源编辑器, sourcePage 来源页, ids 正文标识, annotations 注解标识, page 目标页, copied 新正文标识, copiedNotes 新注解标识
// 返回: error 元数据损坏、结构不一致或提交错误
func (e *Editor) copyRichTextMetadata(ctx context.Context, source *Editor, sourcePage int, ids, annotations []string, page int, copied, copiedNotes []string) error {
if err := ctx.Err(); err != nil {
return err
}
metadata := source.richTextMetadataIndex()
if metadata.err != nil || len(metadata.entries) == 0 {
return metadata.err
}
copyObject := func(before, after GraphicObject) error {
if err := ctx.Err(); err != nil {
return err
}
snapshot, err := source.captureRichTextSnapshot(sourcePage, before)
if err != nil || snapshot == nil {
return err
}
if err := e.putRichTextSnapshot(page, after, snapshot); err != nil {
return err
}
return ctx.Err()
}
for i, id := range ids {
before, err := source.Object(sourcePage, id)
if err != nil {
return err
}
after, err := e.Object(page, copied[i])
if err != nil {
return err
}
if err := copyObject(before, after); err != nil {
return err
}
}
for i, id := range annotations {
before, err := source.Annotation(sourcePage, id)
if err != nil {
return err
}
after, err := e.Annotation(page, copiedNotes[i])
if err != nil {
return err
}
if err := copyObject(GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: CompositeGraphicUnit{Objects: before.Appearance.Objects}}, GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: CompositeGraphicUnit{Objects: after.Appearance.Objects}}); err != nil {
return err
}
}
return nil
}
// richTextColors 为自产RGB颜色指定文档内有效颜色空间,不改变默认颜色空间
// 入参: object 排版生成的标准图元
// 返回: error 颜色或资源错误
func (e *Editor) richTextColors(object *GraphicObject) error {
var colors []*FillColor
switch object.Type {
case "TextObject":
colors = []*FillColor{object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)}
case "PathObject":
colors = []*FillColor{object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)}
default:
return fmt.Errorf("rich text layout must emit standard text or path objects")
}
for _, paint := range colors {
if paint == nil {
continue
}
var r, g, b float64
if _, err := fmt.Sscan(paint.Value, &r, &g, &b); err != nil {
return err
}
rgb, err := e.RGBColor(color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255})
if err != nil {
return err
}
paint.ColorSpace = rgb.ColorSpace
}
return nil
}
// copiedRichTextMetadata 检查当前文档对象快照的标引,不将外部同号对象误认成本地对象
// 入参: objects 待复制快照
// 返回: map[int]*richTextSnapshot 需要复制的语义, error 损坏诊断
func (e *Editor) copiedRichTextMetadata(objects []GraphicObject) (map[int]*richTextSnapshot, error) {
result := make(map[int]*richTextSnapshot)
for i, object := range objects {
if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
continue
}
if object.origin != nil && e.snapshotOrigin(object) == nil {
continue
}
if object.richText != nil {
result[i] = object.richText
continue
}
index := e.richTextMetadataIndex()
if index.err != nil {
return nil, index.err
}
if len(index.entries) == 0 {
continue
}
for page := range e.pages {
snapshot, err := e.captureRichTextSnapshot(page, object)
if err != nil {
return nil, err
}
if snapshot != nil {
result[i] = snapshot
break
}
}
}
return result, nil
}
// attachRichTextSnapshot 为对象快照附加嵌套编辑语义,来源删除后仍可跨页粘贴
// 入参: page 页面索引, object 对象快照
// 返回: error 标引或资源错误
func (e *Editor) attachRichTextSnapshot(page int, object *GraphicObject) error {
snapshot, err := e.captureRichTextSnapshot(page, *object)
object.richText = snapshot
return err
}
// captureRichTextSnapshot 捕获组合树内的富文本标引,不复制共享二进制资源
// 入参: page 页面索引, object 根对象
// 返回: *richTextSnapshot 不可变快照,无标引时为空, error 标引或资源错误
func (e *Editor) captureRichTextSnapshot(page int, object GraphicObject) (*richTextSnapshot, error) {
if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
return nil, nil
}
index := e.richTextMetadataIndex()
if index.err != nil || len(index.entries) == 0 {
return nil, index.err
}
active := make(map[string]bool)
var capture func(GraphicObject) (*richTextSnapshot, error)
capture = func(object GraphicObject) (*richTextSnapshot, error) {
if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
return nil, nil
}
entry, err := e.richTextEntry(page, editorObjectID(object))
if err == nil {
if _, err := e.richTextObject(page, object); err != nil {
return nil, err
}
meta := cloneEditorData(entry.metadata)
return &richTextSnapshot{metadata: &meta}, nil
}
if !errors.Is(err, ErrNotRichText) {
return nil, err
}
unit := object.CompositeGraphicUnit
if unit.ResourceID != "" {
if active[unit.ResourceID] {
return nil, fmt.Errorf("cyclic composite resource")
}
active[unit.ResourceID] = true
defer delete(active, unit.ResourceID)
definition, err := e.compositeDefinition(unit.ResourceID)
if err != nil {
return nil, err
}
unit = definition.object.CompositeGraphicUnit
}
var snapshot *richTextSnapshot
i := 0
for child := range unit.objects() {
value, err := capture(child)
if err != nil {
return nil, err
}
if value != nil {
if snapshot == nil {
snapshot = &richTextSnapshot{children: make(map[int]*richTextSnapshot)}
}
snapshot.children[i] = value
}
i++
}
return snapshot, nil
}
return capture(object)
}
// putRichTextSnapshot 恢复复制后的嵌套标引,按目标标准内容更新摘要和字体映射
// 入参: page 目标页, object 复制后的对象, snapshot 来源不可变语义
// 返回: error 结构或标引错误
func (e *Editor) putRichTextSnapshot(page int, object GraphicObject, snapshot *richTextSnapshot) error {
if snapshot.metadata != nil {
index := -1
if entry, err := e.richTextEntry(page, editorObjectID(object)); err == nil {
index = entry.index
} else if !errors.Is(err, ErrNotRichText) {
return err
}
return e.putCompositeRichTextMetadata(page, object, *snapshot.metadata, index)
}
unit := object.CompositeGraphicUnit
if unit.ResourceID != "" {
definition, err := e.compositeDefinition(unit.ResourceID)
if err != nil {
return err
}
unit = definition.object.CompositeGraphicUnit
}
i, count := 0, 0
for child := range unit.objects() {
if value := snapshot.children[i]; value != nil {
if err := e.putRichTextSnapshot(page, child, value); err != nil {
return err
}
count++
}
i++
}
if count != len(snapshot.children) {
return fmt.Errorf("copied composite structure differs")
}
return nil
}
// richTextTagsFontSafe 仅允许完整识别且不含资源编号的本库标引通过字体重绑定检查
// 入参: ctx 取消上下文, reader 包阅读器, index 文档索引
// 返回: bool 是否可以排除字体引用, error 读取或取消错误
func richTextTagsFontSafe(ctx context.Context, reader *Reader, index int) (bool, error) {
view, err := reader.DocumentContext(ctx, index)
if err != nil {
return false, err
}
defer view.Close()
tags, err := view.CustomTagsContext(ctx)
if err != nil {
return false, err
}
for _, tag := range tags {
if tag.TypeID != richTextTagType || tag.NameSpace != richTextNamespace || tag.SchemaLoc != "" {
return false, nil
}
structure, err := view.readStructureContext(ctx, tag)
if err != nil {
return false, err
}
root := structure.Root
if root.Name != "RichText" || root.Namespace != richTextNamespace || len(root.Attributes) != 0 {
return false, nil
}
if !richTextMetadataFontSafe(root.Text) {
return false, nil
}
for _, child := range root.Children {
if child.Name != "ObjectRef" || child.Namespace != richTextNamespace || len(child.Children) != 0 || len(child.Attributes) != 1 || child.Attributes[0].Namespace != "" || child.Attributes[0].Name != "PageRef" {
return false, nil
}
}
}
return true, ctx.Err()
}
// richTextMetadataFontSafe 核对本库富文本元数据不包含字体资源编号或未知字段
// 入参: value 元数据JSON
// 返回: bool 是否可由标准图元完整统计字体引用
func richTextMetadataFontSafe(value string) bool {
var meta richTextMetadata
decoder := json.NewDecoder(bytes.NewBufferString(value))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&meta); err != nil {
return false
}
if decoder.Decode(new(any)) != io.EOF || meta.Version != 1 {
return false
}
for i := range meta.Value.Runs {
if meta.Value.Runs[i].Format.Font != "" {
return false
}
meta.Value.Runs[i].Format.Font = "1"
}
if err := meta.Value.Validate(); err != nil {
return false
}
return true
}
// formatCompositeText 修改内部富文本格式,普通文字安全提升后保留原实例变换
// 入参: page 页面索引, path 父路径, index 成员序号, patch 格式增量
// 返回: error 识别、排版或提交错误
func (e *Editor) formatCompositeText(page int, path ObjectPath, index int, patch TextFormatPatch) error {
value, err := e.TargetRichText(page, ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: append(slices.Clone(path.Children), index)})
if err != nil {
return err
}
if err := value.ApplyFormat(0, value.Len(), patch); err != nil {
return err
}
return e.UpdateCompositeRichText(page, path, index, value, 0, 0)
}
// updateCompositePlainRichText 将可验证布局的内部文字提升为富文本并保留实例坐标
// 入参: page 页面索引, path 父路径, index 成员序号, value 富文本内容, offset 局部偏移, width 可选宽度, reflow 是否显式重排
// 返回: 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 := nodes[0]
if node.object.Type != "TextObject" || !editorXMLSupported(node.span) {
return ErrNotRichText
}
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, 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)
}
metadata = meta
return node.update(next)
}); err != nil {
return err
}
member, err := e.CompositeObject(page, path, index)
if err != nil {
return err
}
return e.putCompositeRichTextMetadata(page, member.Object, metadata, -1)
}
// TargetRichText 读取完整路径文字的富文本模型,普通文字仅在可验证布局时提升
// 入参: 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 读取独立富文本模型和局部编辑框,定位文字采用自然宽度,已知段落保留宽度
// 显式重排保留原首行起点及页面变换,不从原始Boundary推断段落宽度
// 入参: 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{}, Box{}, Matrix{}, ErrNotRichText
}
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
}
autoWidth := text.layout == nil
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)
if err != nil {
return RichText{}, Box{}, Matrix{}, err
}
value.AutoWidth = autoWidth
if autoWidth {
result, err := e.layoutRichText(value, frame.W)
if err != nil {
return RichText{}, Box{}, Matrix{}, err
}
frame.W, frame.H = result.width, result.bounds.Y+result.bounds.H
}
return value, Box{W: frame.W, H: frame.H}, matrix.Multiply(TranslationMatrix(dx, dy)), nil
}
// 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]
return e.updateCompositeRichText(page, path, index, value, 0, 0, true)
}
// LayoutTextFrame 统一调整普通文字及富文本的段落选项和局部宽度,保留原首行起点与页面变换
// 对齐应用于所有段落;定位文字沿用自然宽度,显式改宽后采用固定宽度,提交一条撤销记录
// 入参: page 页面索引, path 完整对象路径, options 段落选项, offset 左边界局部偏移, width 新宽度,零值保留宽度模式
// 返回: error 路径、尺寸或排版错误
func (e *Editor) LayoutTextFrame(page int, path ObjectPath, options TextLayout, offset, width float64) error {
if !finite(offset) || !finite(width) || width < 0 || width == 0 && offset != 0 {
return fmt.Errorf("invalid text frame")
}
value, _, _, err := e.PrepareRichText(page, path, true)
if err != nil {
return err
}
value.Layout, value.Paragraphs = options, nil
if len(path.Children) == 0 {
return e.updateRichTextFrame(page, path.ID, value, offset, width, true)
}
index := path.Children[len(path.Children)-1]
path.Children = path.Children[:len(path.Children)-1]
return e.updateCompositeRichText(page, path, index, value, offset, width, true)
}