feat(编辑能力): 完善复合对象编辑,修复重复对齐问题
build.yaml / build (push) Successful in 39s

This commit is contained in:
xiaoqidun committed 2026-09-17 15:59:58 +08:00
1 parent 896b00148a
commit 47562c4e7a
5 files changed
+521 -123

No files matched your search

+14 -5
View File
@@ -1005,7 +1005,7 @@ function openObjectStyle() {
const alpha = shared("alpha", 255), dash = shared("dashPattern", ""); const alpha = shared("alpha", 255), dash = shared("dashPattern", "");
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100); el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
el.objectOpacity.placeholder = alpha === null ? "混合" : ""; el.objectOpacity.placeholder = alpha === null ? "混合" : "";
el.objectStrokeFields.hidden = !items.every(item => item.type === "PathObject"); el.objectStrokeFields.hidden = Boolean(state.composite) || !items.every(item => item.type === "PathObject");
el.objectDash.value = dash === null ? "mixed" : dash === "" ? "solid" : "custom"; el.objectDash.value = dash === null ? "mixed" : dash === "" ? "solid" : "custom";
el.objectDashPattern.value = dash || ""; el.objectDashPattern.value = dash || "";
el.objectDashRow.hidden = el.objectDash.value !== "custom"; el.objectDashRow.hidden = el.objectDash.value !== "custom";
@@ -1704,6 +1704,13 @@ async function changeTextStyle(color) {
updateObjectControls(canvasEditor.selected, true); updateObjectControls(canvasEditor.selected, true);
return; return;
} }
if (state.composite) {
if (color && fill !== null && canEditObject(item, "paint")) {
await changeDocument("ofdgoCompositeTextColor", item, fill);
}
updateObjectControls(canvasEditor.selected, true);
return;
}
if (item.items) { if (item.items) {
if (color ? !canEditObject(item, "paint") : !canEditObject(item, "layoutKnown") || !canEditObject(item, "reflow")) return; if (color ? !canEditObject(item, "paint") : !canEditObject(item, "layoutKnown") || !canEditObject(item, "reflow")) return;
if (!color && !el.textSize.value) return; if (!color && !el.textSize.value) return;
@@ -1823,7 +1830,8 @@ async function changeDocument(name, item, ...args) {
return; return;
} }
const scope = state.composite; const scope = state.composite;
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize" }[name]; const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor" }[name];
const members = item?.items || (item ? [item] : []); const members = item?.items || (item ? [item] : []);
const scoped = scope && members.length && members.every(member => member.scoped); const scoped = scope && members.length && members.every(member => member.scoped);
if (!item) resetCompositeScope(); if (!item) resetCompositeScope();
@@ -5498,7 +5506,7 @@ function updateControls() {
} }
function updateEditorTools() { function updateEditorTools() {
el.editButton.disabled = !state.doc || !state.ready || state.exporting; el.editButton.disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
el.editButton.setAttribute("aria-pressed", String(state.editing)); el.editButton.setAttribute("aria-pressed", String(state.editing));
el.editButton.title = state.editing ? "阅读" : "编辑"; el.editButton.title = state.editing ? "阅读" : "编辑";
el.editButton.setAttribute("aria-label", el.editButton.title); el.editButton.setAttribute("aria-label", el.editButton.title);
@@ -5532,7 +5540,7 @@ function updateObjectControls(item, reset = false) {
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting; const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
el.deleteObjectButton.disabled = disabled || !canEditObject(item, "delete"); el.deleteObjectButton.disabled = disabled || !canEditObject(item, "delete");
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy"); el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy");
el.objectStyleButton.disabled = disabled || Boolean(state.composite) || !canEditObject(item, "transform"); el.objectStyleButton.disabled = disabled || !canEditObject(item, "transform");
const cropping = Boolean(canvasEditor.crop); const cropping = Boolean(canvasEditor.crop);
el.objectBoundsButton.disabled = disabled || cropping || !canEditObject(item, "transform"); el.objectBoundsButton.disabled = disabled || cropping || !canEditObject(item, "transform");
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange"); el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
@@ -5551,7 +5559,7 @@ function updateObjectControls(item, reset = false) {
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting; el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
const selection = selectedText(item); const selection = selectedText(item);
const text = selection ? currentTextStyle() : state.textDefaults; const text = selection ? currentTextStyle() : state.textDefaults;
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(state.composite) || Boolean(item?.items && !selection); const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(state.composite && !selection) || Boolean(item?.items && !selection);
fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont")); fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont"));
el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items) && !canEditObject(item, "layoutKnown")); el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items) && !canEditObject(item, "layoutKnown"));
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "paint"); el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "paint");
@@ -5659,6 +5667,7 @@ function showError(err, empty = !state.doc) {
function setBusy(busy, text = "", percent = 0, status = "") { function setBusy(busy, text = "", percent = 0, status = "") {
document.body.toggleAttribute("aria-busy", busy); document.body.toggleAttribute("aria-busy", busy);
el.editButton.disabled = busy || !state.doc || !state.ready || state.exporting;
el.createForm.inert = busy; el.createForm.inert = busy;
el.insertForm.inert = busy; el.insertForm.inert = busy;
el.pageForm.inert = busy; el.pageForm.inert = busy;
+114 -41
View File
@@ -761,34 +761,10 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
return nil, err return nil, err
} }
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "position": position.Index, "count": position.Count, "container": position.Container, item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "position": position.Index, "count": position.Count, "container": position.Container,
"capabilities": map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode)}} "capabilities": editorCapabilities(capability, object.Type)}
if missing := capability.MissingGlyphs; missing != nil { editorAppearance(item, object, capability.Paint, editorPathScale(object.PathObject))
item["capabilities"].(map[string]any)["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
}
item["capabilities"].(map[string]any)["enter"] = capability.Transform && (object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit")
var alpha *int
switch object.Type {
case "TextObject":
alpha = object.TextObject.Alpha
case "ImageObject":
alpha = object.ImageObject.Alpha
item["imageBorder"] = object.ImageObject.Border != nil
case "PathObject":
alpha = object.PathObject.Alpha
case "CompositeObject", "CompositeGraphicUnit":
alpha = object.CompositeGraphicUnit.Alpha
}
item["alpha"] = 255
if alpha != nil {
item["alpha"] = *alpha
}
if object.Type == "PathObject" { if object.Type == "PathObject" {
path := object.PathObject path := object.PathObject
item["dashPattern"], item["cap"], item["join"] = path.DashPattern, path.Cap, path.Join
item["dashOffset"] = 0.0
if path.DashOffset != nil {
item["dashOffset"] = *path.DashOffset
}
kind, geometry := path.Shape() kind, geometry := path.Shape()
if kind != "" && capability.Update { if kind != "" && capability.Update {
item["shape"] = string(kind) item["shape"] = string(kind)
@@ -796,12 +772,6 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
} else if outline, err := path.Outline(); err == nil { } else if outline, err := path.Outline(); err == nil {
item["outline"] = outline item["outline"] = outline
} }
if capability.Paint {
item["fill"], item["stroke"] = path.Fill != nil && *path.Fill, path.Stroke == nil || *path.Stroke
item["fillColor"] = editorColorHex(path.FillColor)
item["strokeColor"] = editorColorHex((*ofdgo.FillColor)(path.StrokeColor))
item["lineWidth"] = path.LineWidth * editorPathScale(path)
}
} }
if object.Type == "PathObject" || object.Type == "ImageObject" || object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" { if object.Type == "PathObject" || object.Type == "ImageObject" || object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
paths := make([]any, len(contours)) paths := make([]any, len(contours))
@@ -849,9 +819,6 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
second, _ := strconv.ParseFloat(codes[1].Y, 64) second, _ := strconv.ParseFloat(codes[1].Y, 64)
item["lineHeight"] = second - first item["lineHeight"] = second - first
} }
if capability.Paint {
item["color"] = editorColorHex(object.TextObject.FillColor)
}
} }
objects = append(objects, item) objects = append(objects, item)
} }
@@ -860,6 +827,52 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
return objects, nil return objects, nil
} }
// editorCapabilities 统一顶层与内部对象的操作能力和受限原因
// 入参: capability 库层能力, kind 对象类型
// 返回: map[string]any 前端能力
func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[string]any {
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
"enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
if missing := capability.MissingGlyphs; missing != nil {
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
}
return result
}
// editorAppearance 提供对象自身透明度及有效绘制外观,不混入父对象透明度
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
var alpha *int
switch object.Type {
case "TextObject":
alpha = object.TextObject.Alpha
if paint {
item["color"] = editorColorHex(object.TextObject.FillColor)
}
case "ImageObject":
alpha = object.ImageObject.Alpha
item["imageBorder"] = object.ImageObject.Border != nil
case "PathObject":
path := object.PathObject
alpha = path.Alpha
item["dashPattern"], item["cap"], item["join"], item["dashOffset"] = path.DashPattern, path.Cap, path.Join, 0.0
if path.DashOffset != nil {
item["dashOffset"] = *path.DashOffset
}
if paint {
item["fill"], item["stroke"] = path.Fill != nil && *path.Fill, path.Stroke == nil || *path.Stroke
item["fillColor"], item["strokeColor"] = editorColorHex(path.FillColor), editorColorHex((*ofdgo.FillColor)(path.StrokeColor))
item["lineWidth"] = path.LineWidth * scale
}
case "CompositeObject", "CompositeGraphicUnit":
alpha = object.CompositeGraphicUnit.Alpha
}
item["alpha"] = 255
if alpha != nil {
item["alpha"] = *alpha
}
}
// compositePath 解析画布复合对象路径 // compositePath 解析画布复合对象路径
// 入参: value 顶层标识与逐层序号 // 入参: value 顶层标识与逐层序号
// 返回: ofdgo.ObjectPath 库层路径, error 错误信息 // 返回: ofdgo.ObjectPath 库层路径, error 错误信息
@@ -892,14 +905,29 @@ func compositeObjects(args []js.Value) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
fonts, err := currentSession.Reader.Fonts()
if err != nil {
return nil, err
}
fontNames := make(map[string]string, len(fonts))
for _, font := range fonts {
fontNames[font.ID] = font.FontName
}
objects := make([]any, 0, len(members)) objects := make([]any, 0, len(members))
for i, member := range members { for i, member := range members {
box := member.Bounds box := member.Bounds
if box.W <= 0 || box.H <= 0 { if box.W <= 0 || box.H <= 0 {
continue continue
} }
objects = append(objects, map[string]any{"id": fmt.Sprintf("%s/%d", key, i), "type": member.Object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": i, "position": i, "count": len(members), "container": key, "scoped": true, "contours": member.Contours, item := map[string]any{"id": fmt.Sprintf("%s/%d", key, i), "type": member.Object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": i, "position": i, "count": len(members), "container": key, "scoped": true, "contours": member.Contours,
"capabilities": map[string]any{"transform": member.Transform, "enter": member.Object.Type == "CompositeObject" || member.Object.Type == "CompositeGraphicUnit"}}) "capabilities": editorCapabilities(member.Capabilities, member.Object.Type)}
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
if member.Object.Type == "TextObject" {
object := member.Object.TextObject
item["text"], _ = object.TextLayout()
item["font"], item["fontName"], item["size"] = object.Font, fontNames[object.Font], object.Size
}
objects = append(objects, item)
} }
return objects, nil return objects, nil
} }
@@ -927,7 +955,8 @@ func changeCompositeObjects(args []js.Value) (any, error) {
indexes = append(indexes, i) indexes = append(indexes, i)
} }
page := args[0].Int() page := args[0].Int()
switch args[3].String() { operation := args[3].String()
switch operation {
case "transform": case "transform":
return currentEditor.TransformCompositeObjects(page, path, indexes, args[4].Float(), args[5].Float(), args[6].Float()) return currentEditor.TransformCompositeObjects(page, path, indexes, args[4].Float(), args[5].Float(), args[6].Float())
case "rotate": case "rotate":
@@ -936,6 +965,44 @@ func changeCompositeObjects(args []js.Value) (any, error) {
return currentEditor.FlipCompositeObjects(page, path, indexes, args[4].String()) return currentEditor.FlipCompositeObjects(page, path, indexes, args[4].String())
case "resize": case "resize":
return currentEditor.ResizeCompositeObjects(page, path, indexes, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()}) return currentEditor.ResizeCompositeObjects(page, path, indexes, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
case "align":
return currentEditor.AlignCompositeObjects(page, path, indexes, args[4].String())
case "distribute":
return currentEditor.DistributeCompositeObjects(page, path, indexes, args[4].String())
case "style":
return currentEditor.StyleCompositeObjects(page, path, indexes, objectStyle(args[4]))
case "paint", "textColor":
style := ofdgo.ObjectStyle{}
if operation == "textColor" {
style.FillColor = &ofdgo.FillColor{}
if err := setEditorColor(style.FillColor, args[4].String()); err != nil {
return err
}
} else {
if !args[4].IsNull() {
value := args[4].Bool()
style.Fill = &value
}
if !args[6].IsNull() {
value := args[6].Bool()
style.Stroke = &value
}
var colors [2]*ofdgo.FillColor
for i := range colors {
if !args[5+i*2].IsNull() {
colors[i] = &ofdgo.FillColor{}
if err := setEditorColor(colors[i], args[5+i*2].String()); err != nil {
return err
}
}
}
style.FillColor, style.StrokeColor = colors[0], (*ofdgo.StrokeColor)(colors[1])
if !args[8].IsNull() {
value := args[8].Float()
style.LineWidth = &value
}
}
return currentEditor.StyleCompositeObjects(page, path, indexes, style)
default: default:
return fmt.Errorf("unsupported composite operation") return fmt.Errorf("unsupported composite operation")
} }
@@ -1321,8 +1388,15 @@ func changeOutline(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func styleObjects(args []js.Value) (any, error) { func styleObjects(args []js.Value) (any, error) {
return changeObjects(func() error { return changeObjects(func() error {
return currentEditor.StyleObjects(args[0].Int(), stringsFromJS(args[1]), objectStyle(args[2]))
})
}
// objectStyle 读取高级面板提交的外观字段,省略字段保持原值
// 入参: value 前端样式
// 返回: ofdgo.ObjectStyle 库层样式
func objectStyle(value js.Value) ofdgo.ObjectStyle {
style := ofdgo.ObjectStyle{} style := ofdgo.ObjectStyle{}
value := args[2]
if field := value.Get("alpha"); !field.IsUndefined() { if field := value.Get("alpha"); !field.IsUndefined() {
v := field.Int() v := field.Int()
style.Alpha = &v style.Alpha = &v
@@ -1343,8 +1417,7 @@ func styleObjects(args []js.Value) (any, error) {
v := field.String() v := field.String()
style.Join = &v style.Join = &v
} }
return currentEditor.StyleObjects(args[0].Int(), stringsFromJS(args[1]), style) return style
})
} }
// clearImport 释放待插页文档 // clearImport 释放待插页文档
+66 -28
View File
@@ -268,6 +268,39 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
if err != nil { if err != nil {
return err return err
} }
matrices, err := editorDistribution(boxes, indexes, axis)
if err != nil {
return err
}
var updates []GraphicObject
for i, matrix := range matrices {
if matrix == IdentityMatrix {
continue
}
object, err := cloneEditorObject(objects[i])
if err != nil {
return err
}
object, err = e.transformObject(object, matrix.e, matrix.f, 1)
if err != nil {
return err
}
updates = append(updates, object)
}
return e.updateObjects(page, updates, true)
}
// editorDistribution 计算等间距平移,位置相同时按原绘制顺序排序
// 入参: boxes 可见范围, indexes 绘制位置, axis 分布方向
// 返回: []Matrix 页面平移, error 错误信息
func editorDistribution(boxes []Box, indexes []editorObjectPosition, axis string) ([]Matrix, error) {
matrices := make([]Matrix, len(boxes))
for i := range matrices {
matrices[i] = IdentityMatrix
}
if len(boxes) < 3 {
return matrices, nil
}
positions, sizes := make([]float64, len(boxes)), make([]float64, len(boxes)) positions, sizes := make([]float64, len(boxes)), make([]float64, len(boxes))
order := make([]int, len(boxes)) order := make([]int, len(boxes))
total := 0.0 total := 0.0
@@ -293,11 +326,10 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
step = 0 step = 0
} }
if step < 0 || step == 0 && compareEditorPosition(indexes[index], indexes[order[i+1]]) > 0 { if step < 0 || step == 0 && compareEditorPosition(indexes[index], indexes[order[i+1]]) > 0 {
return fmt.Errorf("overlap prevents ordered equal-gap distribution") return nil, fmt.Errorf("overlap prevents ordered equal-gap distribution")
} }
} }
next := positions[first] + sizes[first] + gap next := positions[first] + sizes[first] + gap
updates := make([]GraphicObject, 0, len(order)-2)
for _, index := range order[1 : len(order)-1] { for _, index := range order[1 : len(order)-1] {
position := next position := next
next += sizes[index] + gap next += sizes[index] + gap
@@ -308,17 +340,9 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
if axis == "vertical" { if axis == "vertical" {
dx, dy = 0, dx dx, dy = 0, dx
} }
object, err := cloneEditorObject(objects[index]) matrices[index] = TranslationMatrix(dx, dy)
if err != nil {
return err
} }
object, err = e.transformObject(object, dx, dy, 1) return matrices, nil
if err != nil {
return err
}
updates = append(updates, object)
}
return e.updateObjects(page, updates, true)
} }
// replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作 // replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作
@@ -504,26 +528,12 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
} }
} }
for i, box := range boxes { for i, box := range boxes {
dx, dy := 0.0, 0.0 matrix := editorAlignment(box, target, alignment)
switch alignment {
case "left":
dx = target.X - box.X
case "center":
dx = target.X + (target.W-box.W)/2 - box.X
case "right":
dx = target.X + target.W - box.W - box.X
case "top":
dy = target.Y - box.Y
case "middle":
dy = target.Y + (target.H-box.H)/2 - box.Y
case "bottom":
dy = target.Y + target.H - box.H - box.Y
}
objects[i], err = cloneEditorObject(objects[i]) objects[i], err = cloneEditorObject(objects[i])
if err != nil { if err != nil {
return err return err
} }
objects[i], err = e.transformObject(objects[i], dx, dy, 1) objects[i], err = e.transformObject(objects[i], matrix.e, matrix.f, 1)
if err != nil { if err != nil {
return err return err
} }
@@ -531,6 +541,34 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
return e.updateObjects(page, objects, true) return e.updateObjects(page, objects, true)
} }
// editorAlignment 计算对象范围到目标边缘或中心的平移
// 入参: box 对象范围, target 目标范围, alignment 对齐方式
// 返回: Matrix 页面平移
func editorAlignment(box, target Box, alignment string) Matrix {
matrix := IdentityMatrix
switch alignment {
case "left":
matrix.e = target.X - box.X
case "center":
matrix.e = target.X + (target.W-box.W)/2 - box.X
case "right":
matrix.e = target.X + target.W - box.W - box.X
case "top":
matrix.f = target.Y - box.Y
case "middle":
matrix.f = target.Y + (target.H-box.H)/2 - box.Y
case "bottom":
matrix.f = target.Y + target.H - box.H - box.Y
}
if canvas.Equal(matrix.e, 0) {
matrix.e = 0
}
if canvas.Equal(matrix.f, 0) {
matrix.f = 0
}
return matrix
}
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部 // DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部
// 入参: page 页面索引, ids 对象标识 // 入参: page 页面索引, ids 对象标识
// 返回: error 错误信息 // 返回: error 错误信息
+231 -36
View File
@@ -32,11 +32,14 @@ type ObjectPath struct {
Children []int `json:"children,omitempty"` Children []int `json:"children,omitempty"`
} }
// CompositeMember 复合对象的直接成员快照,Bounds与Contours使用页面毫米坐标 // CompositeMember 复合对象的直接成员快照,Object包含有效样式,不用于整体替换原对象
// Bounds与Contours使用页面毫米坐标,StrokeScale为对象描边到页面的倍率
type CompositeMember struct { type CompositeMember struct {
Object GraphicObject Object GraphicObject
Bounds Box Bounds Box
Contours []ObjectContour Contours []ObjectContour
StrokeScale float64
Capabilities ObjectCapabilities
Transform bool Transform bool
} }
@@ -48,8 +51,8 @@ type editorCompositeNode struct {
parent Matrix parent Matrix
boundaryInCTM bool boundaryInCTM bool
defaults *DrawParam defaults *DrawParam
drawParams []string
clip *canvas.Path clip *canvas.Path
alpha *int
visible bool visible bool
ref *editorCompositeNode ref *editorCompositeNode
children []*editorCompositeNode children []*editorCompositeNode
@@ -67,7 +70,7 @@ func (e *Editor) CompositeObjects(page int, path ObjectPath) ([]CompositeMember,
return nil, err return nil, err
} }
defer reader.Close() defer reader.Close()
return measureCompositeMembers(renderer, members), nil return e.measureCompositeMembers(renderer, members), nil
} }
// TransformCompositeObjects 在页面坐标中等比缩放并平移内部选区,只隔离被修改实例的资源 // TransformCompositeObjects 在页面坐标中等比缩放并平移内部选区,只隔离被修改实例的资源
@@ -177,6 +180,7 @@ func (e *Editor) compositeScope(page int, path ObjectPath) (*Reader, *Renderer,
} }
renderer := NewRenderer(reader, WithFontFS(e.fontFS...)) renderer := NewRenderer(reader, WithFontFS(e.fontFS...))
root.parent, root.visible, root.defaults = IdentityMatrix, true, renderer.drawParamDefaults(layer.DrawParam, nil) root.parent, root.visible, root.defaults = IdentityMatrix, true, renderer.drawParamDefaults(layer.DrawParam, nil)
root.drawParams = []string{layer.DrawParam}
scope := root scope := root
for depth := 0; ; depth++ { for depth := 0; ; depth++ {
var members []*editorCompositeNode var members []*editorCompositeNode
@@ -243,7 +247,7 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
} }
clip := intersectClipPath(n.clip, renderer.buildClipPath(clips, 0, 0, 0, clipMatrix)) clip := intersectClipPath(n.clip, renderer.buildClipPath(clips, 0, 0, 0, clipMatrix))
defaults := renderer.drawParamDefaults(c.DrawParam, n.defaults) defaults := renderer.drawParamDefaults(c.DrawParam, n.defaults)
alpha := mergeAlpha(c.Alpha, n.alpha) drawParams := append(slices.Clone(n.drawParams), c.DrawParam)
visible := n.visible && (c.Visible == nil || *c.Visible) visible := n.visible && (c.Visible == nil || *c.Visible)
if c.ResourceID != "" { if c.ResourceID != "" {
data, err := reader.readFile(reader.resourceFiles[c.ResourceID]) data, err := reader.readFile(reader.resourceFiles[c.ResourceID])
@@ -274,7 +278,8 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
if err != nil { if err != nil {
return nil, err return nil, err
} }
n.ref.parent, n.ref.boundaryInCTM, n.ref.defaults, n.ref.clip, n.ref.alpha, n.ref.visible = matrix, true, defaults, clip, alpha, visible n.ref.parent, n.ref.boundaryInCTM, n.ref.defaults, n.ref.clip, n.ref.visible = matrix, true, defaults, clip, visible
n.ref.drawParams = drawParams
} }
var collect func(*editorXML) error var collect func(*editorXML) error
collect = func(container *editorXML) error { collect = func(container *editorXML) error {
@@ -293,7 +298,8 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
if err != nil { if err != nil {
return err return err
} }
item.parent, item.boundaryInCTM, item.defaults, item.clip, item.alpha, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, alpha, visible, child item.parent, item.boundaryInCTM, item.defaults, item.clip, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, visible, child
item.drawParams = drawParams
n.children = append(n.children, item) n.children = append(n.children, item)
} }
} }
@@ -321,53 +327,223 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
return append(result, n.children...), nil return append(result, n.children...), nil
} }
// measureCompositeMembers 按实际父变换、裁剪和继承样式度量成员 // measureCompositeMembers 按父变换、裁剪和继承样式度量成员,透明对象保留选区,未着色路径按轮廓度量
// 入参: renderer 渲染器, nodes 成员 // 入参: renderer 渲染器, nodes 成员
// 返回: []CompositeMember 独立快照及操作能力 // 返回: []CompositeMember 独立快照及操作能力
func measureCompositeMembers(renderer *Renderer, nodes []*editorCompositeNode) []CompositeMember { func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorCompositeNode) []CompositeMember {
result := make([]CompositeMember, len(nodes)) result := make([]CompositeMember, len(nodes))
for i, node := range nodes { for i, node := range nodes {
bounds := &boundsRenderer{collect: true} bounds := &boundsRenderer{collect: true}
object := mergeGraphicObjectAlpha(node.object, node.alpha) object := node.object
object.TextObject.Alpha, object.PathObject.Alpha = nil, nil
object.ImageObject.Alpha, object.CompositeGraphicUnit.Alpha = nil, nil
if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke && (object.PathObject.Fill == nil || !*object.PathObject.Fill) {
object.PathObject.Stroke = nil
}
if node.visible { if node.visible {
renderer.renderObject(canvas.NewContext(bounds), &object, 0, node.defaults, &node.parent, node.boundaryInCTM, node.clip) renderer.renderObject(canvas.NewContext(bounds), &object, 0, node.defaults, &node.parent, node.boundaryInCTM, node.clip)
} }
_, invertible := node.parent.Invert() _, invertible := node.parent.Invert()
borderActions := node.object.Type == "ImageObject" && node.object.ImageObject.Border != nil && len(node.object.ImageObject.Actions) != 0 borderActions := node.object.Type == "ImageObject" && node.object.ImageObject.Border != nil && len(node.object.ImageObject.Actions) != 0
result[i] = CompositeMember{Object: cloneEditorData(node.object), Bounds: bounds.box, Contours: bounds.contours, Transform: invertible && editorXMLTransformable(node.node) && validateEditorGeometry(node.object) == nil && !borderActions} transform := invertible && editorXMLTransformable(node.node) && validateEditorGeometry(node.object) == nil && !borderActions
capability := ObjectCapabilities{Transform: transform, Arrange: transform && bounds.box.W > 0 && bounds.box.H > 0}
style, err := e.compositeMemberStyle(node)
if !transform {
capability.ReasonCode, capability.Reason = EditUnsupportedContainer, "composite member cannot be transformed"
} else if err != nil {
capability.ReasonCode, capability.Reason = editReason(err), err.Error()
} else {
capability.Paint = e.editorPaintable(style)
}
if err != nil {
style = cloneEditorData(node.object)
}
_, ctm := editorGeometry(node.object)
matrix := node.parent.Multiply(NewMatrix(ctm))
result[i] = CompositeMember{Object: style, Bounds: bounds.box, Contours: bounds.contours, StrokeScale: math.Sqrt(math.Abs(matrix.a*matrix.d - matrix.b*matrix.c)), Capabilities: capability, Transform: transform}
} }
return result return result
} }
// compositeMemberStyle 解析成员的继承样式,缺失或循环参数不开放改色
// 入参: node 成员节点
// 返回: GraphicObject 有效外观, error 错误信息
func (e *Editor) compositeMemberStyle(node *editorCompositeNode) (GraphicObject, error) {
for _, id := range node.drawParams {
if _, err := e.editorDrawParam(id, make(map[string]bool)); err != nil {
return GraphicObject{}, err
}
}
base := node.defaults
if base == nil {
base = &DrawParam{}
}
object, err := e.resolveEditorStyleDefaults(node.object, base)
if err == nil && object.Type == "PathObject" && object.PathObject.LineWidth == 0 {
object.PathObject.LineWidth = defaultPathLineWidth
}
return object, err
}
// StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽
// 不重排文字,不改写继承参数;暂不支持虚线及端点样式
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米
// 返回: error 错误信息
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) error {
if style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil {
return fmt.Errorf("composite stroke parameters are not supported")
}
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil
for i, node := range nodes {
capability := members[i].Capabilities
if !capability.Transform || paint && !capability.Paint {
return fmt.Errorf("composite member %d cannot accept this style: %w", indexes[i], capability.editError())
}
local := style
switch members[i].Object.Type {
case "TextObject":
local.FillColor = editorStyleColor(style.FillColor, members[i].Object.TextObject.FillColor)
case "PathObject":
p := members[i].Object.PathObject
local.FillColor = editorStyleColor(style.FillColor, p.FillColor)
local.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(p.StrokeColor)))
if style.LineWidth != nil {
width := *style.LineWidth / members[i].StrokeScale
local.LineWidth = &width
}
}
object, err := e.styleObject(node.object, local)
if err != nil {
return err
}
data, err := editorXMLObject(node.data, node.node, node.object, object)
if err != nil {
return err
}
if bytes.Equal(data, node.data) {
continue
}
updated, err := newEditorCompositeNode(data)
if err != nil {
return err
}
node.data, node.node, node.object, node.changed = data, updated.node, updated.object, true
}
return nil
})
}
// AlignCompositeObjects 将内部多选成员对齐至选区边界,单选时对齐页面
// 入参: page 页面索引, path 父路径, indexes 成员序号, alignment 为left、center、right、top、middle或bottom
// 返回: error 错误信息
func (e *Editor) AlignCompositeObjects(page int, path ObjectPath, indexes []int, alignment string) error {
if !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, alignment) {
return fmt.Errorf("invalid alignment %q", alignment)
}
return e.arrangeCompositeObjects(page, path, indexes, func(boxes []Box) ([]Matrix, error) {
target, _ := ParseBox(e.pages[page].Area.PhysicalBox)
if len(boxes) > 1 {
target = Box{}
for _, box := range boxes {
target = unionTextBox(target, box)
}
}
matrices := make([]Matrix, len(boxes))
for i, box := range boxes {
matrices[i] = editorAlignment(box, target, alignment)
}
return matrices, nil
})
}
// DistributeCompositeObjects 按可见范围等距分布内部成员,固定两端并保留绘制顺序
// 入参: page 页面索引, path 父路径, indexes 成员序号, axis 为horizontal或vertical
// 返回: error 错误信息
func (e *Editor) DistributeCompositeObjects(page int, path ObjectPath, indexes []int, axis string) error {
if axis != "horizontal" && axis != "vertical" {
return fmt.Errorf("invalid distribution axis %q", axis)
}
return e.arrangeCompositeObjects(page, path, indexes, func(boxes []Box) ([]Matrix, error) {
positions := make([]editorObjectPosition, len(indexes))
for i, index := range indexes {
positions[i].index = index
}
return editorDistribution(boxes, positions, axis)
})
}
// arrangeCompositeObjects 按成员实际轮廓计算独立页面变换并提交一次历史记录
// 入参: page 页面索引, path 父路径, indexes 成员序号, arrange 排列计算
// 返回: error 错误信息
func (e *Editor) arrangeCompositeObjects(page int, path ObjectPath, indexes []int, arrange func([]Box) ([]Matrix, error)) error {
return e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
boxes := make([]Box, len(members))
for i, member := range members {
if !member.Capabilities.Arrange {
return fmt.Errorf("composite member has no editable bounds")
}
boxes[i] = member.Bounds
}
matrices, err := arrange(boxes)
if err != nil {
return err
}
for i, matrix := range matrices {
if err := e.transformCompositeMember(renderer, nodes[i], matrix); err != nil {
return err
}
}
return nil
})
}
// changeCompositeObjects 原子变换内部选区,沿改动路径复制共享资源 // changeCompositeObjects 原子变换内部选区,沿改动路径复制共享资源
// 入参: page 页面索引, path 父路径, indexes 成员序号, transform 页面变换生成器 // 入参: page 页面索引, path 父路径, indexes 成员序号, transform 页面变换生成器
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int, transform func(Box) Matrix) (err error) { func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int, transform func(Box) Matrix) error {
return e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
var box Box
for i := range nodes {
if !members[i].Capabilities.Transform {
return members[i].Capabilities.editError()
}
box = unionTextBox(box, members[i].Bounds)
}
matrix := transform(box)
for _, node := range nodes {
if err := e.transformCompositeMember(renderer, node, matrix); err != nil {
return err
}
}
return nil
})
}
// editCompositeObjects 校验选区并原子提交成员改动,失败时回收本次分配的资源
// 入参: page 页面索引, path 父路径, indexes 成员序号, edit 修改回调
// 返回: error 错误信息
func (e *Editor) editCompositeObjects(page int, path ObjectPath, indexes []int, edit func(*Renderer, []*editorCompositeNode, []CompositeMember) error) (err error) {
reader, renderer, root, nodes, err := e.compositeScope(page, path) reader, renderer, root, nodes, err := e.compositeScope(page, path)
if err != nil { if err != nil {
return err return err
} }
defer reader.Close() defer reader.Close()
members := measureCompositeMembers(renderer, nodes) members := e.measureCompositeMembers(renderer, nodes)
selected := make(map[int]bool) selected := make(map[int]bool)
var box Box var selectedNodes []*editorCompositeNode
var selectedMembers []CompositeMember
for _, i := range indexes { for _, i := range indexes {
if i < 0 || i >= len(nodes) || selected[i] { if i < 0 || i >= len(nodes) || selected[i] {
return fmt.Errorf("invalid composite selection") return fmt.Errorf("invalid composite selection")
} }
if !members[i].Transform {
return fmt.Errorf("composite member %d cannot be transformed", i)
}
selected[i] = true selected[i] = true
box = unionTextBox(box, members[i].Bounds) selectedNodes = append(selectedNodes, nodes[i])
selectedMembers = append(selectedMembers, members[i])
} }
if len(selected) == 0 { if len(selected) == 0 {
return nil return nil
} }
matrix := transform(box)
if matrix == IdentityMatrix {
return nil
}
count, maximum := len(e.resources), e.maxID count, maximum := len(e.resources), e.maxID
ready := e.source.idsReady ready := e.source.idsReady
defer func() { defer func() {
@@ -380,8 +556,30 @@ func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int
if err = e.prepareSourceIDs(); err != nil { if err = e.prepareSourceIDs(); err != nil {
return err return err
} }
for i := range selected { if err = edit(renderer, selectedNodes, selectedMembers); err != nil {
n := nodes[i] return err
}
data, changed, err := e.writeCompositeNode(root)
if err != nil || !changed {
return err
}
next, err := newEditorCompositeNode(data)
if err != nil {
return err
}
origin := e.objectOrigin(path.ID)
next.node.parent = origin.node.parent
updated := &editorObjectOrigin{page: origin.page, data: data, node: next.node, object: next.object}
return e.updateObjectOrigins(page, []GraphicObject{next.object}, true, map[string]*editorObjectOrigin{path.ID: updated})
}
// transformCompositeMember 将页面变换换算至成员坐标,保留边框及旧式边界约定
// 入参: renderer 渲染器, n 成员, matrix 页面变换
// 返回: error 错误信息
func (e *Editor) transformCompositeMember(renderer *Renderer, n *editorCompositeNode, matrix Matrix) (err error) {
if matrix == IdentityMatrix {
return nil
}
object := cloneEditorData(n.object) object := cloneEditorData(n.object)
inverse, _ := n.parent.Invert() inverse, _ := n.parent.Invert()
basic := object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" basic := object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit"
@@ -419,19 +617,7 @@ func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int
return err return err
} }
n.node, n.object, n.changed = updated.node, updated.object, true n.node, n.object, n.changed = updated.node, updated.object, true
} return nil
data, _, err := e.writeCompositeNode(root)
if err != nil {
return err
}
next, err := newEditorCompositeNode(data)
if err != nil {
return err
}
origin := e.objectOrigin(path.ID)
next.node.parent = origin.node.parent
updated := &editorObjectOrigin{page: origin.page, data: data, node: next.node, object: next.object}
return e.updateObjectOrigins(page, []GraphicObject{next.object}, true, map[string]*editorObjectOrigin{path.ID: updated})
} }
// transformCompositeOffsets 变换旧式内联复合成员的页面偏移,资源引用保持原坐标约定 // transformCompositeOffsets 变换旧式内联复合成员的页面偏移,资源引用保持原坐标约定
@@ -632,14 +818,23 @@ func (e *Editor) compositeResource(id string, data []byte) (editorResource, erro
func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) { func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) {
used[composite.ResourceID] = true used[composite.ResourceID] = true
for _, object := range composite.Objects { for _, object := range composite.Objects {
var fill, stroke *FillColor
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
used[object.TextObject.Font] = true used[object.TextObject.Font] = true
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
case "PathObject":
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
case "ImageObject": case "ImageObject":
used[object.ImageObject.ResourceID] = true used[object.ImageObject.ResourceID] = true
used[object.ImageObject.ImageMask] = true used[object.ImageObject.ImageMask] = true
case "CompositeObject", "CompositeGraphicUnit": case "CompositeObject", "CompositeGraphicUnit":
collectCompositeReferences(object.CompositeGraphicUnit, used) collectCompositeReferences(object.CompositeGraphicUnit, used)
} }
for _, color := range []*FillColor{fill, stroke} {
if color != nil {
used[color.ColorSpace] = true
}
}
} }
} }
+96 -13
View File
@@ -66,6 +66,22 @@ func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (Graphic
if err != nil { if err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
return e.resolveEditorStyleDefaults(object, base)
}
// resolveEditorStyleDefaults 合并对象参数与继承外观,仅生成编辑快照
// 入参: object 原对象, base 继承参数
// 返回: GraphicObject 有效样式快照, error 错误信息
func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawParam) (GraphicObject, error) {
var id string
switch object.Type {
case "TextObject":
id = object.TextObject.DrawParam
case "PathObject":
id = object.PathObject.DrawParam
default:
return cloneEditorData(object), nil
}
dp, err := e.editorDrawParam(id, make(map[string]bool)) dp, err := e.editorDrawParam(id, make(map[string]bool))
if err != nil { if err != nil {
return GraphicObject{}, err return GraphicObject{}, err
@@ -101,37 +117,77 @@ func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (Graphic
return cloneEditorObject(object) return cloneEditorObject(object)
} }
// ObjectStyle 对象外观,nil字段保持原值,长度单位为对象坐标系中的毫米 // ObjectStyle 对象外观,nil字段保持原值,颜色的Alpha为nil时保留原颜色透明度
// StyleObjects使用对象坐标毫米,StyleCompositeObjects的LineWidth使用页面毫米
type ObjectStyle struct { type ObjectStyle struct {
Alpha *int Alpha *int
Fill *bool
Stroke *bool
FillColor *FillColor
StrokeColor *StrokeColor
LineWidth *float64
DashPattern *string DashPattern *string
DashOffset *float64 DashOffset *float64
Cap *string Cap *string
Join *string Join *string
} }
// StyleObjects 原子更新文字、图片、路径与复合对象透明度,路径另支持描边样式 // StyleObjects 原子更新对象透明度、文字颜色及路径填充和描边样式
// 入参: page 页面索引, ids 对象标识, style 待修改属性 // 入参: page 页面索引, ids 对象标识, style 待修改属性
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error { func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
if style.Alpha != nil && (*style.Alpha < 0 || *style.Alpha > 255) {
return fmt.Errorf("alpha must be between 0 and 255")
}
objects, _, err := e.selectedObjects(page, ids) objects, _, err := e.selectedObjects(page, ids)
if err != nil { if err != nil {
return err return err
} }
stroke := style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
for i := range objects { for i := range objects {
object := cloneEditorData(objects[i]) if style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil {
if stroke && object.Type != "PathObject" { capability, err := e.ObjectCapabilities(page, ids[i])
return fmt.Errorf("stroke style requires path objects") if err != nil {
return err
}
if !capability.Paint {
return fmt.Errorf("object %q cannot be painted: %w", ids[i], capability.editError())
}
}
objects[i], err = e.styleObject(objects[i], style)
if err != nil {
return err
}
}
return e.updateObjects(page, objects, true)
}
// styleObject 仅替换请求的外观字段,保留内容、定位及未修改样式
// 入参: object 原对象, style 待修改属性
// 返回: GraphicObject 修改后的独立对象, error 错误信息
func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicObject, error) {
if style.Alpha != nil && (*style.Alpha < 0 || *style.Alpha > 255) {
return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255")
}
stroke := style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
paint := style.Fill != nil || style.Stroke != nil || style.StrokeColor != nil || style.LineWidth != nil
if (stroke || paint) && object.Type != "PathObject" {
return GraphicObject{}, fmt.Errorf("stroke style requires path objects")
}
if style.FillColor != nil && object.Type != "TextObject" && object.Type != "PathObject" {
return GraphicObject{}, fmt.Errorf("fill color requires text or path objects")
}
for _, value := range []*FillColor{style.FillColor, (*FillColor)(style.StrokeColor)} {
if value != nil {
if err := e.editorColor(value); err != nil {
return GraphicObject{}, err
}
}
} }
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
if style.Alpha != nil { if style.Alpha != nil {
object.TextObject.Alpha = style.Alpha object.TextObject.Alpha = style.Alpha
} }
if style.FillColor != nil {
object.TextObject.FillColor = editorStyleColor(style.FillColor, object.TextObject.FillColor)
}
case "ImageObject": case "ImageObject":
if style.Alpha != nil { if style.Alpha != nil {
object.ImageObject.Alpha = style.Alpha object.ImageObject.Alpha = style.Alpha
@@ -145,6 +201,24 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
if style.Alpha != nil { if style.Alpha != nil {
path.Alpha = style.Alpha path.Alpha = style.Alpha
} }
if style.Fill != nil {
path.Fill = style.Fill
}
if style.Stroke != nil {
path.Stroke = style.Stroke
}
if style.FillColor != nil {
path.FillColor = editorStyleColor(style.FillColor, path.FillColor)
}
if style.StrokeColor != nil {
path.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(path.StrokeColor)))
}
if style.LineWidth != nil {
if !finite(*style.LineWidth) || *style.LineWidth <= 0 {
return GraphicObject{}, fmt.Errorf("line width must be positive")
}
path.LineWidth = *style.LineWidth
}
if style.DashPattern != nil { if style.DashPattern != nil {
path.DashPattern = *style.DashPattern path.DashPattern = *style.DashPattern
} }
@@ -159,15 +233,24 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
} }
if stroke { if stroke {
if err := validateEditorStroke(*path); err != nil { if err := validateEditorStroke(*path); err != nil {
return err return GraphicObject{}, err
} }
} }
default: default:
return fmt.Errorf("unsupported object type %q", object.Type) return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
} }
objects[i] = object return cloneEditorData(object), nil
} }
return e.updateObjects(page, objects, true)
// editorStyleColor 替换色值时保留未指定的颜色透明度
// 入参: color 新颜色, before 原有效颜色
// 返回: *FillColor 独立颜色
func editorStyleColor(color, before *FillColor) *FillColor {
result := cloneEditorData(color)
if result != nil && result.Alpha == nil && before != nil {
result.Alpha = cloneEditorData(before.Alpha)
}
return result
} }
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作 // CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作