mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
f8eeaad9f2
commit
4826496e98
13 files changed
+447
-106
No files matched your search
@@ -106,12 +106,12 @@
|
||||
<button id="objectBoundsButton" class="editor-group-action" type="button" title="位置尺寸" aria-label="位置尺寸" aria-haspopup="dialog" aria-controls="objectBoundsPanel" disabled>变换</button>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
<button id="insertTextButton" class="button" type="button" aria-label="添加文字" aria-pressed="false">文字</button>
|
||||
<button id="insertImageButton" class="button" type="button" aria-label="添加图片">图片</button>
|
||||
<button id="drawLineButton" class="button" type="button" aria-label="绘制直线" aria-pressed="false">直线</button>
|
||||
<button id="drawRectangleButton" class="button" type="button" aria-label="绘制矩形" aria-pressed="false">矩形</button>
|
||||
<button id="drawEllipseButton" class="button" type="button" aria-label="绘制椭圆" aria-pressed="false">椭圆</button>
|
||||
<select id="arrowTool" aria-label="箭头" title="箭头"><option value="" hidden>箭头</option><option value="line">无</option><option value="arrow">单向</option><option value="double-arrow">双向</option></select>
|
||||
<button id="insertTextButton" class="button" type="button" title="文字" aria-label="文字" aria-pressed="false">文字</button>
|
||||
<button id="insertImageButton" class="button" type="button" title="图片" aria-label="图片">图片</button>
|
||||
<button id="drawLineButton" class="button" type="button" title="直线" aria-label="直线" aria-pressed="false">直线</button>
|
||||
<button id="drawRectangleButton" class="button" type="button" title="矩形" aria-label="矩形" aria-pressed="false">矩形</button>
|
||||
<button id="drawEllipseButton" class="button" type="button" title="椭圆" aria-label="椭圆" aria-pressed="false">椭圆</button>
|
||||
<select id="arrowTool" aria-label="箭头" title="箭头"><option value="" hidden>箭头</option><option value="line">直线</option><option value="arrow">单向</option><option value="double-arrow">双向</option></select>
|
||||
<span>插入</span>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
|
||||
+11
-4
@@ -678,7 +678,7 @@ el.copyStyleButton.addEventListener("click", async () => {
|
||||
const item = canvasEditor.selected, seq = state.openSeq;
|
||||
setBusy(true);
|
||||
try {
|
||||
await callWASM("ofdgoCaptureStyle", item.index, item.id);
|
||||
await callWASM("ofdgoCaptureStyle", item.index, item.id, state.composite?.key || "");
|
||||
if (seq === state.openSeq) { state.styleClipboard = item.type; el.objectStylePanel.close(); }
|
||||
} catch (err) { if (seq === state.openSeq) el.objectStyleStatus.textContent = err.message; }
|
||||
finally { if (seq === state.openSeq) setBusy(false); }
|
||||
@@ -1006,11 +1006,18 @@ function openObjectBounds() {
|
||||
el.objectBoundsPanel.showModal();
|
||||
}
|
||||
|
||||
function canCopyStyle(item) {
|
||||
if (!item?.scoped) return canEditObject(item, "update");
|
||||
if (item.type === "TextObject") return canEditObject(item, "replaceFont") && canEditObject(item, "paint");
|
||||
if (item.type === "PathObject") return canEditObject(item, "transform") && canEditObject(item, "paint");
|
||||
return item.type === "ImageObject" && canEditObject(item, "transform");
|
||||
}
|
||||
|
||||
function openObjectStyle() {
|
||||
const items = canvasEditor.items();
|
||||
if (!items.length) return;
|
||||
el.copyStyleButton.disabled = items.length !== 1 || !canEditObject(items[0], "update");
|
||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canEditObject(item, "update"));
|
||||
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||
const alpha = shared("alpha", 255), dash = shared("dashPattern", "");
|
||||
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
||||
@@ -1867,7 +1874,7 @@ async function changeDocument(name, item, ...args) {
|
||||
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
|
||||
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
|
||||
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoCropImage: "crop", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
|
||||
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath",
|
||||
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath", ofdgoPasteStyle: "pasteStyle",
|
||||
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order" }[name];
|
||||
const members = item?.items || (item ? [item] : []);
|
||||
const scoped = scope && members.length && members.every(member => member.scoped);
|
||||
|
||||
+31
-5
@@ -983,15 +983,20 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
||||
page := args[0].Int()
|
||||
operation := args[3].String()
|
||||
switch operation {
|
||||
case "pasteStyle":
|
||||
if copiedStyle == nil {
|
||||
return fmt.Errorf("style clipboard is empty")
|
||||
}
|
||||
return currentEditor.CopyCompositeStyle(page, path, indexes, *copiedStyle)
|
||||
case "reshape", "line":
|
||||
if len(indexes) != 1 {
|
||||
return fmt.Errorf("line editing requires one member")
|
||||
return fmt.Errorf("shape editing requires one member")
|
||||
}
|
||||
kind := ofdgo.ShapeKind("")
|
||||
box := ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()}
|
||||
if operation == "line" {
|
||||
kind = ofdgo.ShapeKind(args[8].String())
|
||||
return currentEditor.ReshapeCompositeLine(page, path, indexes[0], ofdgo.ShapeKind(args[8].String()), box)
|
||||
}
|
||||
return currentEditor.ReshapeCompositeLine(page, path, indexes[0], kind, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
|
||||
return currentEditor.ReshapeCompositeObject(page, path, indexes[0], box)
|
||||
case "erase":
|
||||
return currentEditor.EraseCompositeObjects(page, path, indexes, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
|
||||
case "erasePath":
|
||||
@@ -1566,12 +1571,33 @@ func moveOutline(args []js.Value) (any, error) {
|
||||
}
|
||||
|
||||
// captureStyle 保存单个对象样式来源,不改写文档
|
||||
// 入参: args 页面索引和对象标识
|
||||
// 入参: args 页面索引、对象标识及可选内部范围
|
||||
// 返回: any 空结果, error 错误信息
|
||||
func captureStyle(args []js.Value) (any, error) {
|
||||
if currentEditor == nil {
|
||||
return nil, fmt.Errorf("no document is being edited")
|
||||
}
|
||||
if len(args) > 2 && args[2].String() != "" {
|
||||
key := args[2].String()
|
||||
path, err := compositePath(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
indexes, err := compositeIndexes(key, []string{args[1].String()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
members, err := currentEditor.CompositeObjects(args[0].Int(), path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if indexes[0] >= len(members) {
|
||||
return nil, fmt.Errorf("style source is unavailable")
|
||||
}
|
||||
object := members[indexes[0]].Style()
|
||||
copiedStyle = &object
|
||||
return nil, nil
|
||||
}
|
||||
object, err := currentEditor.Object(args[0].Int(), args[1].String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -77,6 +77,7 @@ type editorResource struct {
|
||||
subset *editorFontSubset
|
||||
states map[string]editorCompositeState
|
||||
draw *DrawParam
|
||||
drawSource string
|
||||
}
|
||||
|
||||
// definition 获取独立定义资源的标识
|
||||
|
||||
@@ -464,9 +464,7 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
||||
if node.visible {
|
||||
renderer.renderObject(canvas.NewContext(bounds), &object, 0, node.defaults, &node.parent, node.boundaryInCTM, node.clip)
|
||||
}
|
||||
_, invertible := node.parent.Invert()
|
||||
borderActions := node.object.Type == "ImageObject" && node.object.ImageObject.Border != nil && len(node.object.ImageObject.Actions) != 0
|
||||
transform := invertible && editorXMLTransformable(node.node) && validateEditorGeometry(node.object) == nil && !borderActions
|
||||
transform := node.transformable()
|
||||
capability := ObjectCapabilities{Transform: transform, Arrange: transform && bounds.box.W > 0 && bounds.box.H > 0,
|
||||
Delete: true, Copy: transform && editorXMLCopyable(node.node), Order: orderable[node.span.parent], ReplaceImage: node.object.Type == "ImageObject"}
|
||||
style, err := e.compositeMemberStyle(node)
|
||||
@@ -507,6 +505,14 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
||||
return result
|
||||
}
|
||||
|
||||
// transformable 校验内部对象能否保留原文进行几何操作,不执行绘制测量
|
||||
// 返回: bool 是否支持
|
||||
func (n *editorCompositeNode) transformable() bool {
|
||||
_, invertible := n.parent.Invert()
|
||||
borderActions := n.object.Type == "ImageObject" && n.object.ImageObject.Border != nil && len(n.object.ImageObject.Actions) != 0
|
||||
return invertible && editorXMLTransformable(n.node) && validateEditorGeometry(n.object) == nil && !borderActions
|
||||
}
|
||||
|
||||
// compositeMemberStyle 解析成员的继承样式,缺失或循环参数不开放改色
|
||||
// 入参: node 成员节点
|
||||
// 返回: GraphicObject 有效外观, error 错误信息
|
||||
|
||||
@@ -87,6 +87,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
||||
}
|
||||
copy.setStates(source.states)
|
||||
copy.defaults = source.defaults
|
||||
copy.drawParams = source.drawParams
|
||||
object, err := e.compositeMemberStyle(source)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -251,7 +252,7 @@ func editorClipPath(path *canvas.Path) PathObject {
|
||||
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号
|
||||
// 返回: *CompositeSelection 独立快照, error 错误信息
|
||||
func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []int) (*CompositeSelection, error) {
|
||||
reader, renderer, root, nodes, err := e.compositeScope(page, path)
|
||||
reader, _, root, nodes, err := e.compositeScope(page, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -261,15 +262,17 @@ func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []in
|
||||
}
|
||||
indexes = slices.Clone(indexes)
|
||||
slices.Sort(indexes)
|
||||
members := e.measureCompositeMembers(renderer, nodes)
|
||||
var owner *editorCompositeNode
|
||||
var container *editorXML
|
||||
selection := &CompositeSelection{editor: e, page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Children: slices.Clone(path.Children)}, source: e.objectOrigin(path.ID).page}
|
||||
for j, i := range indexes {
|
||||
if i < 0 || i >= len(nodes) || j > 0 && indexes[j-1] == i || !members[i].Capabilities.Copy {
|
||||
if i < 0 || i >= len(nodes) || j > 0 && indexes[j-1] == i {
|
||||
return nil, fmt.Errorf("invalid composite copy selection")
|
||||
}
|
||||
node := nodes[i]
|
||||
if !node.transformable() || !editorXMLCopyable(node.node) {
|
||||
return nil, fmt.Errorf("composite member cannot be copied")
|
||||
}
|
||||
if j == 0 {
|
||||
owner, container = node.owner, node.span.parent
|
||||
} else if node.span.parent != container {
|
||||
|
||||
@@ -98,10 +98,7 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
||||
if err := node.convertCoordinates(IdentityMatrix, true); err != nil {
|
||||
return err
|
||||
}
|
||||
node.defaults, err = e.editorDrawParam(e.copiedLayerStyle(source), make(map[string]bool))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node.drawParams = []string{e.copiedLayerStyle(source)}
|
||||
}
|
||||
if err := e.isolateCompositeStyle(node); err != nil {
|
||||
return err
|
||||
@@ -157,27 +154,17 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
|
||||
if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" {
|
||||
own, err := e.editorDrawParam(node.object.CompositeGraphicUnit.DrawParam, make(map[string]bool))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node.defaults != nil {
|
||||
own = mergeDrawParam(*node.defaults, own)
|
||||
}
|
||||
for _, color := range []*FillColor{own.FillColor, (*FillColor)(own.StrokeColor)} {
|
||||
if err := e.editorColor(color); err != nil {
|
||||
return &EditError{Code: EditUnsupportedColor, Err: fmt.Errorf("composite inherited color cannot be isolated: %w", err)}
|
||||
}
|
||||
}
|
||||
id, err := e.neutralDrawParam()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base, err := e.editorDrawParam(id, make(map[string]bool))
|
||||
for _, source := range node.drawParams {
|
||||
id, err = e.copyDrawParam(source, id, make(map[string]bool))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err = e.addEditorDrawParam(*mergeDrawParam(*base, own))
|
||||
}
|
||||
id, err = e.copyDrawParam(node.object.CompositeGraphicUnit.DrawParam, id, make(map[string]bool))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -183,26 +183,36 @@ func (n *editorCompositeNode) update(object GraphicObject) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shape 获取内部直线的页面端点,其他成员返回空类型
|
||||
// 返回: ShapeKind 直线或箭头类型, Box 起点及有符号端点位移
|
||||
// Shape 获取内部基本路径的页面几何,矩形和椭圆要求轴对齐,其他成员返回空类型
|
||||
// 返回: ShapeKind 图形类型, Box 几何范围,直线使用起点及有符号端点位移
|
||||
func (m CompositeMember) Shape() (ShapeKind, Box) {
|
||||
if m.Object.Type != "PathObject" || !m.Capabilities.Transform {
|
||||
return "", Box{}
|
||||
}
|
||||
kind, box := m.Object.PathObject.Shape()
|
||||
if !lineShapeKind(kind) {
|
||||
if kind == "" {
|
||||
return "", Box{}
|
||||
}
|
||||
return kind, transformLineBox(box, m.lineParent())
|
||||
parent, ok := m.shapeParent()
|
||||
if !ok {
|
||||
return "", Box{}
|
||||
}
|
||||
if lineShapeKind(kind) {
|
||||
return kind, transformLineBox(box, parent)
|
||||
}
|
||||
if !axisAlignedMatrix(parent) {
|
||||
return "", Box{}
|
||||
}
|
||||
return kind, parent.TransformBox(box)
|
||||
}
|
||||
|
||||
// lineParent 获取直线所在坐标到页面的变换,兼容旧式内联边界
|
||||
// 返回: Matrix 直线父坐标变换
|
||||
func (m CompositeMember) lineParent() Matrix {
|
||||
// shapeParent 获取路径所在坐标到页面的变换,兼容旧式内联边界
|
||||
// 返回: Matrix 路径父坐标变换, bool 是否可逆
|
||||
func (m CompositeMember) shapeParent() (Matrix, bool) {
|
||||
p := m.Object.PathObject
|
||||
box, _ := ParseBox(p.Boundary)
|
||||
inverse, _ := TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(p.CTM)).Invert()
|
||||
return m.Matrix.Multiply(inverse)
|
||||
inverse, ok := TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(p.CTM)).Invert()
|
||||
return m.Matrix.Multiply(inverse), ok
|
||||
}
|
||||
|
||||
// transformLineBox 变换起点及有符号端点位移
|
||||
@@ -218,10 +228,27 @@ func transformLineBox(box Box, matrix Matrix) Box {
|
||||
// 入参: page 页面索引, path 父路径, index 成员序号, kind 直线类型, box 页面端点
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) ReshapeCompositeLine(page int, path ObjectPath, index int, kind ShapeKind, box Box) error {
|
||||
if kind != "" && !lineShapeKind(kind) {
|
||||
return fmt.Errorf("unsupported line kind %q", kind)
|
||||
}
|
||||
return e.reshapeCompositeObject(page, path, index, kind, box, true)
|
||||
}
|
||||
|
||||
// ReshapeCompositeObject 调整内部基本路径的页面几何,保留绘制样式、变换与裁剪
|
||||
// 入参: page 页面索引, path 父路径, index 成员序号, box 页面范围,直线使用有符号端点位移
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) ReshapeCompositeObject(page int, path ObjectPath, index int, box Box) error {
|
||||
return e.reshapeCompositeObject(page, path, index, "", box, false)
|
||||
}
|
||||
|
||||
// reshapeCompositeObject 按页面几何修改单个内部路径,可选修改箭头类型
|
||||
// 入参: page 页面索引, path 父路径, index 成员序号, kind 目标类型,空值保持原类型, box 页面几何, lineOnly 是否仅接受直线
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) reshapeCompositeObject(page int, path ObjectPath, index int, kind ShapeKind, box Box, lineOnly bool) error {
|
||||
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||
before, geometry := members[0].Shape()
|
||||
if before == "" {
|
||||
return fmt.Errorf("composite member is not a supported line")
|
||||
if before == "" || lineOnly && !lineShapeKind(before) {
|
||||
return fmt.Errorf("composite member is not a supported shape")
|
||||
}
|
||||
if kind == "" {
|
||||
kind = before
|
||||
@@ -236,7 +263,14 @@ func (e *Editor) ReshapeCompositeLine(page int, path ObjectPath, index int, kind
|
||||
object = compositeBoundary(object, node.parent, true)
|
||||
}
|
||||
var err error
|
||||
if lineShapeKind(kind) {
|
||||
object.PathObject, err = object.PathObject.ReshapeLine(kind, transformLineBox(box, inverse))
|
||||
} else {
|
||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||
return err
|
||||
}
|
||||
object.PathObject, err = object.PathObject.Reshape(inverse.TransformBox(box))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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"
|
||||
|
||||
// Style 获取内部成员的独立样式快照,路径CTM只表达页面描边倍率
|
||||
// 可传入CopyStyle或CopyCompositeStyle,不作为几何复制快照
|
||||
// 返回: GraphicObject 有效样式来源
|
||||
func (m CompositeMember) Style() GraphicObject {
|
||||
object := cloneEditorData(m.Object)
|
||||
if object.Type == "PathObject" {
|
||||
object.PathObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
// CopyCompositeStyle 将同类型样式复制到内部选区,保留内容、位置、裁剪及其他实例
|
||||
// 路径复制页面实际线宽和虚线,文字沿用目标段落和基线,全部成功后提交一次撤销
|
||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, source Object或CompositeMember.Style取得的样式快照
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) CopyCompositeStyle(page int, path ObjectPath, indexes []int, source GraphicObject) error {
|
||||
if err := e.validateCopyStyle(source); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||
for i, node := range nodes {
|
||||
member := members[i]
|
||||
if node.object.Type != source.Type || !member.Capabilities.Transform || source.Type != "ImageObject" && !member.Capabilities.Paint {
|
||||
return fmt.Errorf("style requires editable members of the same type")
|
||||
}
|
||||
object := cloneEditorData(member.Object)
|
||||
switch source.Type {
|
||||
case "TextObject":
|
||||
if err := e.updateCompositeText(node, member, nil, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size}, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
object, err = e.compositeMemberStyle(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object.TextObject.FillColor = cloneEditorData(source.TextObject.FillColor)
|
||||
object.TextObject.Alpha = cloneEditorData(source.TextObject.Alpha)
|
||||
object.TextObject.DrawParam, err = e.neutralDrawParam()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "PathObject":
|
||||
object.PathObject = copyEditorPathStyle(object.PathObject, source.PathObject, editorStrokeScale(source.PathObject.CTM)/member.StrokeScale)
|
||||
if err := validateEditorStroke(object.PathObject); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
object.PathObject.DrawParam, err = e.neutralDrawParam()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "ImageObject":
|
||||
object.ImageObject.Alpha = cloneEditorData(source.ImageObject.Alpha)
|
||||
}
|
||||
if err := node.update(object); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -57,7 +57,18 @@ func (e *Editor) ResizeCompositeTextFrame(page int, path ObjectPath, index int,
|
||||
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 {
|
||||
member := members[i]
|
||||
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 node.object.Type != "TextObject" || !member.Capabilities.ReplaceFont {
|
||||
return fmt.Errorf("composite text style is not supported")
|
||||
}
|
||||
@@ -131,9 +142,7 @@ func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, v
|
||||
return err
|
||||
}
|
||||
node.setState(state)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// layoutCompositeText 重排普通文字并保持原首行基线起点
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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 (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// drawParamXML 读取绘制参数原文,保留复杂颜色、扩展属性及命名空间
|
||||
// 入参: id 绘制参数标识
|
||||
// 返回: []byte 独立参数片段, error 错误信息
|
||||
func (e *Editor) drawParamXML(id string) ([]byte, error) {
|
||||
var data []byte
|
||||
for _, resource := range e.resources {
|
||||
if resource.draw != nil && resource.draw.ID == id {
|
||||
data = resource.data
|
||||
break
|
||||
}
|
||||
}
|
||||
if data == nil && e.source != nil {
|
||||
var err error
|
||||
data, err = e.source.reader.readFile(e.source.reader.resourceFiles[id])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if params := root.child("DrawParams"); params != nil {
|
||||
for _, node := range params.children {
|
||||
if node.name.Local == "DrawParam" && node.attr("ID") == id && (node.name.Space == "" || node.name.Space == ofdNamespace || node.name.Space == "http://www.ofdspec.org") {
|
||||
return editorXMLStandalone(data[node.start:node.end], node)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("draw parameter %q not found", id)
|
||||
}
|
||||
|
||||
// copyDrawParam 将原绘制参数继承链接到独立基准,复用相同副本且不改写原资源
|
||||
// 入参: id 源参数标识, base 独立基准标识, visited 当前继承链
|
||||
// 返回: string 新继承链末端标识, error 错误信息
|
||||
func (e *Editor) copyDrawParam(id, base string, visited map[string]bool) (string, error) {
|
||||
if id == "" || id == base {
|
||||
return base, nil
|
||||
}
|
||||
if visited[id] {
|
||||
return "", fmt.Errorf("cyclic draw parameter %q", id)
|
||||
}
|
||||
visited[id] = true
|
||||
data, err := e.drawParamXML(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if root.attr("Relative") == base {
|
||||
return id, nil
|
||||
}
|
||||
base, err = e.copyDrawParam(root.attr("Relative"), base, visited)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, resource := range e.resources {
|
||||
if resource.drawSource == id && resource.draw.Relative == base {
|
||||
return resource.draw.ID, nil
|
||||
}
|
||||
}
|
||||
if !editorXMLCopyable(root) {
|
||||
return "", fmt.Errorf("draw parameter contains unsupported identifiers")
|
||||
}
|
||||
before, err := editorXMLContainer("DrawParam", ofdAttrs{{Name: xml.Name{Local: "Relative"}, Value: root.attr("Relative")}}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
after, err := editorXMLContainer("DrawParam", ofdAttrs{{Name: xml.Name{Local: "Relative"}, Value: base}}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err = editorXMLMerge(data, root, before, after)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
root, err = parseEditorXML(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ids := make(map[string]string)
|
||||
if err := e.collectCompositeIDs(root, ids); err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err = editorXMLRemapIDs(data, root, ids)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var draw DrawParam
|
||||
if err := xml.Unmarshal(data, &draw); err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err = editorXMLContainer("DrawParams", nil, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err = editorXMLContainer("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
refs, err := editorResourceReferences(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
e.resources = append(e.resources, editorResource{name: e.resourceDirectory() + "/DrawParam_" + draw.ID + ".xml", data: data, draw: &draw, drawSource: id, references: refs})
|
||||
return draw.ID, nil
|
||||
}
|
||||
+101
-47
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
@@ -269,23 +270,12 @@ func editorStyleColor(color, before *FillColor) *FillColor {
|
||||
// 路径按页面中的实际线宽和虚线长度复制,保持目标变换
|
||||
// 入参: page 目标页面索引, ids 目标对象标识, source 样式来源快照
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) error {
|
||||
var alpha *int
|
||||
switch source.Type {
|
||||
case "TextObject":
|
||||
alpha = source.TextObject.Alpha
|
||||
case "PathObject":
|
||||
alpha = source.PathObject.Alpha
|
||||
case "ImageObject":
|
||||
alpha = source.ImageObject.Alpha
|
||||
default:
|
||||
return fmt.Errorf("unsupported style source %q", source.Type)
|
||||
}
|
||||
if alpha != nil && (*alpha < 0 || *alpha > 255) {
|
||||
return fmt.Errorf("alpha must be between 0 and 255")
|
||||
func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) (err error) {
|
||||
if err := e.validateCopyStyle(source); err != nil {
|
||||
return err
|
||||
}
|
||||
objects, _, err := e.selectedObjects(page, ids)
|
||||
if err != nil {
|
||||
if err != nil || len(objects) == 0 {
|
||||
return err
|
||||
}
|
||||
for _, object := range objects {
|
||||
@@ -293,6 +283,33 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) error {
|
||||
return fmt.Errorf("style requires objects of the same type")
|
||||
}
|
||||
}
|
||||
count, maximum := len(e.resources), e.maxID
|
||||
ready := e.source != nil && e.source.idsReady
|
||||
defer func() {
|
||||
if err != nil {
|
||||
clear(e.resources[count:])
|
||||
e.resources, e.maxID = e.resources[:count], maximum
|
||||
if e.source != nil {
|
||||
e.source.idsReady = ready
|
||||
}
|
||||
}
|
||||
}()
|
||||
if source.Type == "TextObject" && source.TextObject.FillColor == nil || source.Type == "PathObject" && (source.PathObject.FillColor == nil || source.PathObject.StrokeColor == nil) {
|
||||
black, err := e.RGBColor(color.NRGBA{A: 255})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if source.Type == "TextObject" {
|
||||
source.TextObject.FillColor = black
|
||||
} else {
|
||||
if source.PathObject.FillColor == nil {
|
||||
source.PathObject.FillColor = black
|
||||
}
|
||||
if source.PathObject.StrokeColor == nil {
|
||||
source.PathObject.StrokeColor = (*StrokeColor)(black)
|
||||
}
|
||||
}
|
||||
}
|
||||
if source.Type == "TextObject" {
|
||||
objects, err = e.styleTextObjects(objects, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size})
|
||||
if err != nil {
|
||||
@@ -304,42 +321,11 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) error {
|
||||
}
|
||||
return e.UpdateObjects(page, objects)
|
||||
}
|
||||
if source.Type == "PathObject" {
|
||||
from := source.PathObject
|
||||
if from.DrawParam != "" {
|
||||
return fmt.Errorf("style source must have resolved draw parameters")
|
||||
}
|
||||
if err := validateEditorStroke(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, color := range []*FillColor{from.FillColor, (*FillColor)(from.StrokeColor)} {
|
||||
if err := e.editorColor(color); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, object := range objects {
|
||||
object = cloneEditorData(object)
|
||||
switch source.Type {
|
||||
case "PathObject":
|
||||
from, to := source.PathObject, &object.PathObject
|
||||
to.Fill, to.Stroke, to.Alpha = from.Fill, from.Stroke, from.Alpha
|
||||
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor
|
||||
to.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
|
||||
to.DashPattern, to.DashOffset = from.DashPattern, from.DashOffset
|
||||
to.dashPatternSet = from.dashPatternSet
|
||||
scale := editorStrokeScale(from.CTM) / editorStrokeScale(to.CTM)
|
||||
if scale != 1 {
|
||||
if to.LineWidth == 0 {
|
||||
to.LineWidth = defaultPathLineWidth
|
||||
}
|
||||
to.LineWidth *= scale
|
||||
to.DashPattern = scaleTextNumbers(to.DashPattern, scale)
|
||||
if to.DashOffset != nil {
|
||||
value := *to.DashOffset * scale
|
||||
to.DashOffset = &value
|
||||
}
|
||||
}
|
||||
object.PathObject = copyEditorPathStyle(object.PathObject, source.PathObject, editorStrokeScale(source.PathObject.CTM)/editorStrokeScale(object.PathObject.CTM))
|
||||
case "ImageObject":
|
||||
object.ImageObject.Alpha = source.ImageObject.Alpha
|
||||
}
|
||||
@@ -348,6 +334,74 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) error {
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// validateCopyStyle 校验样式来源,不分配资源或改写对象
|
||||
// 入参: source 有效外观快照
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) validateCopyStyle(source GraphicObject) error {
|
||||
var alpha *int
|
||||
var colors []*FillColor
|
||||
switch source.Type {
|
||||
case "TextObject":
|
||||
alpha, colors = source.TextObject.Alpha, []*FillColor{source.TextObject.FillColor}
|
||||
case "PathObject":
|
||||
from := source.PathObject
|
||||
alpha, colors = from.Alpha, []*FillColor{from.FillColor, (*FillColor)(from.StrokeColor)}
|
||||
if from.DrawParam != "" {
|
||||
return fmt.Errorf("style source must have resolved draw parameters")
|
||||
}
|
||||
if err := validateEditorStroke(from); err != nil {
|
||||
return err
|
||||
}
|
||||
case "ImageObject":
|
||||
alpha = source.ImageObject.Alpha
|
||||
default:
|
||||
return fmt.Errorf("unsupported style source %q", source.Type)
|
||||
}
|
||||
if alpha != nil && (*alpha < 0 || *alpha > 255) {
|
||||
return fmt.Errorf("alpha must be between 0 and 255")
|
||||
}
|
||||
for _, color := range colors {
|
||||
if err := e.editorColor(color); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyEditorPathStyle 按页面描边倍率复制路径外观,保留目标几何和行为
|
||||
// 入参: to 目标路径, from 来源路径, scale 来源与目标的描边倍率比
|
||||
// 返回: PathObject 独立路径副本
|
||||
func copyEditorPathStyle(to, from PathObject, scale float64) PathObject {
|
||||
from = cloneEditorData(from)
|
||||
to.Fill, to.Stroke, to.Alpha = from.Fill, from.Stroke, from.Alpha
|
||||
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor
|
||||
to.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
|
||||
to.DashPattern, to.DashOffset, to.dashPatternSet = from.DashPattern, from.DashOffset, true
|
||||
if to.LineWidth == 0 {
|
||||
to.LineWidth = defaultPathLineWidth
|
||||
}
|
||||
if to.Cap == "" {
|
||||
to.Cap = "Butt"
|
||||
}
|
||||
if to.Join == "" {
|
||||
to.Join = "Miter"
|
||||
}
|
||||
if to.MiterLimit == 0 {
|
||||
to.MiterLimit = defaultMiterLimit
|
||||
}
|
||||
if to.DashOffset == nil {
|
||||
zero := 0.0
|
||||
to.DashOffset = &zero
|
||||
}
|
||||
if scale != 1 {
|
||||
to.LineWidth *= scale
|
||||
to.DashPattern = scaleTextNumbers(to.DashPattern, scale)
|
||||
value := *to.DashOffset * scale
|
||||
to.DashOffset = &value
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
// editorStrokeScale 获取路径在页面坐标中的描边倍率,与渲染器保持一致
|
||||
// 入参: ctm 对象变换
|
||||
// 返回: float64 描边倍率
|
||||
|
||||
+10
-6
@@ -114,7 +114,7 @@ func NewShape(kind ShapeKind, box Box) (PathObject, error) {
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// Shape 识别 NewShape 生成的基本路径及其轴对齐缩放、直角旋转、镜像和平移
|
||||
// Shape 识别NewShape生成的基本路径,直线支持可逆仿射变换,矩形和椭圆要求轴对齐
|
||||
// 直线范围以起点和有符号的端点位移表示;非基本路径或不支持的变换返回空类型
|
||||
// 返回: ShapeKind 图形类型, Box 几何范围(不含描边)
|
||||
func (p PathObject) Shape() (ShapeKind, Box) {
|
||||
@@ -170,7 +170,8 @@ func (p PathObject) Shape() (ShapeKind, Box) {
|
||||
}
|
||||
boundary, err := ParseBox(p.Boundary)
|
||||
m := NewMatrix(p.CTM)
|
||||
if err != nil || !axisAlignedMatrix(m) {
|
||||
_, invertible := m.Invert()
|
||||
if err != nil || !axisAlignedMatrix(m) && (!line || !invertible) {
|
||||
return "", Box{}
|
||||
}
|
||||
if line {
|
||||
@@ -207,7 +208,7 @@ func sameShapePath(a, b []string) bool {
|
||||
}
|
||||
|
||||
// Reshape 调整基本路径在所在坐标系中的几何范围,保留对象标识、方向、缩放和绘制属性,不缩放线宽
|
||||
// 返回对象可通过 Editor.UpdateObject 写入;仅支持 Shape 可识别的路径
|
||||
// 返回对象可通过Editor.UpdateObject写入;仅支持Shape可识别的路径
|
||||
// 入参: box 新几何范围,直线使用起点和有符号的端点位移
|
||||
// 返回: PathObject 调整后的对象, error 错误信息
|
||||
func (p PathObject) Reshape(box Box) (PathObject, error) {
|
||||
@@ -249,12 +250,15 @@ func (p PathObject) reshape(kind ShapeKind, box Box) (PathObject, error) {
|
||||
}
|
||||
m := NewMatrix(p.CTM)
|
||||
var w, h float64
|
||||
if m.b == 0 {
|
||||
line := lineShapeKind(kind)
|
||||
if m.b == 0 && m.c == 0 {
|
||||
w, h = box.W/m.a, box.H/m.d
|
||||
} else {
|
||||
} else if m.a == 0 && m.d == 0 {
|
||||
w, h = box.H/m.b, box.W/m.c
|
||||
} else {
|
||||
inverse, _ := m.Invert()
|
||||
w, h = matrixVector(inverse, box.W, box.H)
|
||||
}
|
||||
line := kind == ShapeLine || kind == ShapeArrow || kind == ShapeDoubleArrow
|
||||
if !line {
|
||||
w, h = math.Abs(w), math.Abs(h)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user