mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
187 lines
8.0 KiB
Go
187 lines
8.0 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 (
|
|
"fmt"
|
|
"image/color"
|
|
"strconv"
|
|
)
|
|
|
|
// UpdateCompositeText 修改内部文字,原文保留定位,已知段落按会话选项重排
|
|
// 空样式保留原字体、字号和颜色,显式改变字号时重新排版并保留首行基线
|
|
// 方向和字形映射沿用RewriteText的定位校验,不改变原文中未修改的属性
|
|
// 入参: page 页面索引, path 父复合路径, index 成员序号, value 文字, style 文字样式增量
|
|
// 返回: error 错误信息
|
|
func (e *Editor) UpdateCompositeText(page int, path ObjectPath, index int, value string, style TextStyle) error {
|
|
return e.changeCompositeText(page, path, []int{index}, &value, style, nil, nil)
|
|
}
|
|
|
|
// StyleCompositeText 批量修改内部文字字体、字号或颜色,换字体保留原文字定位
|
|
// 更换字体时同步字形索引,无法逐字映射时需显式重排,缺字使整批操作失败
|
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, style 文字样式增量
|
|
// 返回: error 错误信息
|
|
func (e *Editor) StyleCompositeText(page int, path ObjectPath, indexes []int, style TextStyle) error {
|
|
return e.changeCompositeText(page, path, indexes, nil, style, nil, nil)
|
|
}
|
|
|
|
// LayoutCompositeText 显式重排内部普通文字,保留原始基线、变换、裁剪和绘制属性
|
|
// 段落选项仅保留在编辑会话中,撤销重做随对象恢复,保存为标准TextCode定位
|
|
// 入参: page 页面索引, path 父路径, index 成员序号, value 原文, options 段落选项
|
|
// 返回: error 错误信息
|
|
func (e *Editor) LayoutCompositeText(page int, path ObjectPath, index int, value string, options TextLayout) error {
|
|
return e.changeCompositeText(page, path, []int{index}, &value, TextStyle{}, &options, nil)
|
|
}
|
|
|
|
// ResizeCompositeTextFrame 调整内部文字框本地宽度并按指定段落选项重排
|
|
// 入参: page 页面索引, path 父路径, index 成员序号, offset 左侧偏移, width 宽度, options 段落选项
|
|
// 返回: error 错误信息
|
|
func (e *Editor) ResizeCompositeTextFrame(page int, path ObjectPath, index int, offset, width float64, options TextLayout) error {
|
|
return e.changeCompositeText(page, path, []int{index}, nil, TextStyle{}, &options, &Box{X: offset, W: width})
|
|
}
|
|
|
|
// changeCompositeText 生成独立文字副本,全部校验通过后合并原文
|
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, value 新内容,nil保留定位, style 样式增量, options 显式段落选项, frame 可选文字框调整
|
|
// 返回: error 错误信息
|
|
func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, value *string, style TextStyle, options *TextLayout, frame *Box) error {
|
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
|
for i, node := range nodes {
|
|
if err := e.updateCompositeText(node, members[i], value, style, options, frame); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// updateCompositeText 按有效样式更新单个内部文字节点并保留段落基线
|
|
// 入参: node 原文节点, member 有效快照, value 新内容, style 样式增量, options 段落选项, frame 文字框调整
|
|
// 返回: error 错误信息
|
|
func (e *Editor) updateCompositeText(node *editorCompositeNode, member CompositeMember, value *string, style TextStyle, options *TextLayout, frame *Box) error {
|
|
if style.Reflow {
|
|
options = &TextLayout{AutoBidi: true}
|
|
}
|
|
rewriteOnly := value != nil && options == nil && frame == nil && style.Color == "" && (style.Font == "" || style.Font == member.Object.TextObject.Font) && (style.Size == 0 || style.Size == member.Object.TextObject.Size)
|
|
if node.object.Type != "TextObject" || !member.Capabilities.ReplaceFont && !(rewriteOnly && member.Capabilities.RewriteText) {
|
|
return fmt.Errorf("composite text style is not supported")
|
|
}
|
|
object := cloneEditorData(node.object)
|
|
text := cloneEditorData(member.Object.TextObject)
|
|
state := node.states[editorObjectID(node.object)]
|
|
content, layout := text.TextLayout()
|
|
original := content
|
|
if style.Font != "" {
|
|
if state.layout == nil && options == nil && (style.Size == 0 || style.Size == text.Size) {
|
|
replacement := content
|
|
if value != nil {
|
|
replacement = *value
|
|
}
|
|
if err := e.RewriteTextWithFont(&text, replacement, style.Font); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
text.Font = style.Font
|
|
}
|
|
}
|
|
if value != nil {
|
|
content = *value
|
|
}
|
|
if options != nil {
|
|
layout = *options
|
|
}
|
|
if frame != nil {
|
|
var err error
|
|
if !node.boundaryInCTM {
|
|
text = compositeBoundary(GraphicObject{Type: "TextObject", TextObject: text}, node.parent, true).TextObject
|
|
}
|
|
text, err = text.ResizeTextFrame(frame.X, frame.W)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !node.boundaryInCTM {
|
|
text = compositeBoundary(GraphicObject{Type: "TextObject", TextObject: text}, node.parent, false).TextObject
|
|
}
|
|
object.TextObject.Boundary, object.TextObject.CTM = text.Boundary, text.CTM
|
|
}
|
|
if options == nil && frame == nil && state.layout == nil && (style.Size == 0 || style.Size == text.Size) && content != original {
|
|
if err := e.RewriteText(&text, content); err != nil {
|
|
return err
|
|
}
|
|
object.TextObject.TextCode = text.TextCode
|
|
object.TextObject.CGTransform = text.CGTransform
|
|
} else if options != nil || content != original || style.Size != 0 && style.Size != text.Size || state.layout != nil && state.layout.options.Shape && style.Font != "" && style.Font != object.TextObject.Font {
|
|
if style.Size != 0 {
|
|
text.Size = style.Size
|
|
}
|
|
if state.layout == nil {
|
|
if len(text.TextCode) == 0 {
|
|
return fmt.Errorf("text codes are empty")
|
|
}
|
|
origin, err := creationNumbers(text.TextCode[0].X+" "+text.TextCode[0].Y, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
copy(state.origin[:], origin)
|
|
}
|
|
if err := e.layoutCompositeText(&text, content, layout, state.origin); err != nil {
|
|
return err
|
|
}
|
|
object.TextObject.Size, object.TextObject.TextCode = text.Size, text.TextCode
|
|
object.TextObject.CGTransform = text.CGTransform
|
|
state.layout = &textLayout{value: content, options: layout}
|
|
}
|
|
if style.Font != "" && style.Font != object.TextObject.Font {
|
|
if err := e.prepareText(&text); err != nil {
|
|
return err
|
|
}
|
|
object.TextObject.Font = style.Font
|
|
object.TextObject.CGTransform = text.CGTransform
|
|
}
|
|
if style.Color != "" {
|
|
if err := creationColor(&FillColor{Value: style.Color}); err != nil {
|
|
return err
|
|
}
|
|
var r, g, b float64
|
|
fmt.Sscan(style.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 err
|
|
}
|
|
object.TextObject.FillColor = editorStyleColor(fill, member.Object.TextObject.FillColor)
|
|
}
|
|
if err := node.update(object); err != nil {
|
|
return err
|
|
}
|
|
node.setState(state)
|
|
return nil
|
|
}
|
|
|
|
// layoutCompositeText 重排普通文字并保持原首行基线起点
|
|
// 入参: text 有效文字样式, value 新内容, options 段落选项, origin 原始基线起点
|
|
// 返回: error 错误信息
|
|
func (e *Editor) layoutCompositeText(text *TextObject, value string, options TextLayout, origin [2]float64) error {
|
|
if err := e.LayoutText(text, value, options); err != nil {
|
|
return err
|
|
}
|
|
baseline, _ := strconv.ParseFloat(text.TextCode[0].Y, 64)
|
|
for i := range text.TextCode {
|
|
code := &text.TextCode[i]
|
|
cx, _ := strconv.ParseFloat(code.X, 64)
|
|
cy, _ := strconv.ParseFloat(code.Y, 64)
|
|
code.X, code.Y = ofdNumber(cx+origin[0]), ofdNumber(cy+origin[1]-baseline)
|
|
}
|
|
return e.prepareText(text)
|
|
}
|