feat(编辑能力): 完善复合对象编辑,修复复制层级
build.yaml / build (push) Successful in 38s

This commit is contained in:
xiaoqidun committed 2026-09-17 16:47:13 +08:00
1 parent 47562c4e7a
commit 2ddde73b65
16 files changed
+363 -64

No files matched your search

+1 -1
View File
@@ -63,11 +63,11 @@
<span>文件</span> <span>文件</span>
</div> </div>
<div class="editor-group editor-objects"> <div class="editor-group editor-objects">
<button id="compositeBackButton" class="button" type="button" title="返回上层" hidden>返回</button>
<button id="selectObjectButton" class="button" type="button" aria-pressed="true">选择</button> <button id="selectObjectButton" class="button" type="button" aria-pressed="true">选择</button>
<button id="multiSelectButton" class="button" type="button" aria-pressed="false" title="多选对象">多选</button> <button id="multiSelectButton" class="button" type="button" aria-pressed="false" title="多选对象">多选</button>
<button id="editObjectButton" class="button" type="button" aria-label="修改对象" disabled>修改</button> <button id="editObjectButton" class="button" type="button" aria-label="修改对象" disabled>修改</button>
<button id="copyObjectButton" class="button" type="button" aria-label="复制对象" disabled>复制</button> <button id="copyObjectButton" class="button" type="button" aria-label="复制对象" disabled>复制</button>
<button id="compositeBackButton" class="button" type="button" title="返回上层" hidden>返回</button>
<button id="deleteObjectButton" class="button" type="button" disabled>删除</button> <button id="deleteObjectButton" class="button" type="button" disabled>删除</button>
<select id="objectOrder" class="order-select" aria-label="对象层级" disabled> <select id="objectOrder" class="order-select" aria-label="对象层级" disabled>
<option value="" hidden>层级</option> <option value="" hidden>层级</option>
+18 -10
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 = Boolean(state.composite) || !items.every(item => item.type === "PathObject"); el.objectStrokeFields.hidden = !items.every(item => item.type === "PathObject" && (!item.scoped || canEditObject(item, "paint")));
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";
@@ -1313,7 +1313,7 @@ async function editCanvasObject(item) {
if (canEditObject(item, "enter")) return enterCompositeScope(item.index, item.id); if (canEditObject(item, "enter")) return enterCompositeScope(item.index, item.id);
if (item.type === "TextObject") { if (item.type === "TextObject") {
if (!confirmTextReflow(item)) return; if (!confirmTextReflow(item)) return;
} else if (!canEditObject(item, "update")) return; } else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
if (item.type === "PathObject") { if (item.type === "PathObject") {
el.shapeWidth.focus(); el.shapeWidth.focus();
return; return;
@@ -1425,7 +1425,7 @@ function openInsertPanel(item = null) {
} }
el.insertForm.reset(); el.insertForm.reset();
state.insertObject = item; state.insertObject = item;
el.insertFitRow.hidden = !item; el.insertFitRow.hidden = !item || Boolean(item.scoped);
el.insertPanel.setAttribute("aria-label", item ? "替换图片" : "添加图片"); el.insertPanel.setAttribute("aria-label", item ? "替换图片" : "添加图片");
el.insertSubmit.textContent = item ? "替换" : "添加"; el.insertSubmit.textContent = item ? "替换" : "添加";
el.insertStatus.textContent = ""; el.insertStatus.textContent = "";
@@ -1614,8 +1614,9 @@ async function insertObject(event) {
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
const doc = item const doc = item?.scoped
? await callWASM("ofdgoReplaceImage", item.index, item.id, data, el.insertFit.value) ? await callWASM("ofdgoChangeCompositeObjects", item.index, state.composite.key, [item.id], "image", data)
: item ? await callWASM("ofdgoReplaceImage", item.index, item.id, data, el.insertFit.value)
: await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2); : await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2);
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
@@ -1831,7 +1832,8 @@ async function changeDocument(name, item, ...args) {
} }
const scope = state.composite; const scope = state.composite;
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize", 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]; ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order" }[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();
@@ -1872,6 +1874,7 @@ async function changeDocument(name, item, ...args) {
setPan(false); setPan(false);
canvasEditor.pendingSelection = { index: item?.index ?? args[0], ids: doc.selectedIDs }; canvasEditor.pendingSelection = { index: item?.index ?? args[0], ids: doc.selectedIDs };
} }
if (scoped && name === "ofdgoOrderObjects") canvasEditor.pendingSelection = { index: item.index, ids: doc.selectedIDs };
openSeq = ++state.openSeq; openSeq = ++state.openSeq;
if (item || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") { if (item || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection); await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
@@ -3694,6 +3697,11 @@ function copyEditorSelection(event) {
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false; || canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false;
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order); const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
const cut = event.type === "cut"; const cut = event.type === "cut";
if (state.composite) {
event.preventDefault();
setStatus("内部对象请用复制按钮");
return true;
}
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) { if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
event.preventDefault(); event.preventDefault();
setStatus(cut ? "对象不可剪切" : "对象不可复制"); setStatus(cut ? "对象不可剪切" : "对象不可复制");
@@ -5536,10 +5544,11 @@ function updateEditorTools() {
function updateObjectControls(item, reset = false) { function updateObjectControls(item, reset = false) {
el.compositeBackButton.hidden = !state.composite; el.compositeBackButton.hidden = !state.composite;
el.compositeBackButton.disabled = !state.ready || state.exporting; el.compositeBackButton.disabled = !state.ready || state.exporting;
el.deleteObjectButton.hidden = Boolean(state.composite); el.selectObjectButton.hidden = Boolean(state.composite);
const members = item?.items || (item ? [item] : []);
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") || Boolean(state.composite && members.some(member => member.container !== members[0].container));
el.objectStyleButton.disabled = disabled || !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");
@@ -5555,7 +5564,7 @@ function updateObjectControls(item, reset = false) {
el.resetCropButton.disabled = el.cropImageButton.disabled || !cropping && (!item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9)); el.resetCropButton.disabled = el.cropImageButton.disabled || !cropping && (!item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3; el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject" el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : "update")); || !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
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;
@@ -5608,7 +5617,6 @@ function updateObjectControls(item, reset = false) {
el.shapeWidth.value = "1"; el.shapeWidth.value = "1";
} }
updateDrawingControls(); updateDrawingControls();
const members = item?.items || (item ? [item] : []);
const orders = members.map(member => member.position).sort((a, b) => a - b); const orders = members.map(member => member.position).sort((a, b) => a - b);
const count = members[0]?.count || 0; const count = members[0]?.count || 0;
const canRaise = orders.some((order, i) => order !== count - orders.length + i); const canRaise = orders.some((order, i) => order !== count - orders.length + i);
+2 -1
View File
@@ -190,7 +190,8 @@ export function objectEditReason(item) {
if (!reason) return ""; if (!reason) return "";
const message = labels[object.capabilities.reasonCode] || "暂不支持此对象特性"; const message = labels[object.capabilities.reasonCode] || "暂不支持此对象特性";
const available = object.capabilities.transform ? object.capabilities.paint ? "仍可移动、改色" : "仍可移动" const available = object.capabilities.transform ? object.capabilities.paint ? "仍可移动、改色" : "仍可移动"
: object.capabilities.copy && object.capabilities.delete ? "仍可复制、删除" : "暂不可编辑"; : object.capabilities.replaceImage ? "仍可替换图片" : object.capabilities.copy && object.capabilities.delete ? "仍可复制、删除"
: object.capabilities.delete ? "仍可删除" : "暂不可编辑";
return object.capabilities.missingGlyphs ? missingGlyphMessage(object.capabilities.missingGlyphs, available) : `${message},${available}`; return object.capabilities.missingGlyphs ? missingGlyphMessage(object.capabilities.missingGlyphs, available) : `${message},${available}`;
}); });
return [...new Set(reasons.filter(Boolean))].join(";"); return [...new Set(reasons.filter(Boolean))].join(";");
+33 -4
View File
@@ -832,7 +832,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
// 返回: map[string]any 前端能力 // 返回: map[string]any 前端能力
func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) 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), 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")} "replaceImage": capability.ReplaceImage, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
if missing := capability.MissingGlyphs; missing != nil { if missing := capability.MissingGlyphs; missing != nil {
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters} result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
} }
@@ -919,7 +919,7 @@ func compositeObjects(args []js.Value) (any, error) {
if box.W <= 0 || box.H <= 0 { if box.W <= 0 || box.H <= 0 {
continue continue
} }
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, 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": member.Position.Index, "count": member.Position.Count, "container": key + ":" + member.Position.Container, "scoped": true, "contours": member.Contours,
"capabilities": editorCapabilities(member.Capabilities, member.Object.Type)} "capabilities": editorCapabilities(member.Capabilities, member.Object.Type)}
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale) editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
if member.Object.Type == "TextObject" { if member.Object.Type == "TextObject" {
@@ -932,11 +932,12 @@ func compositeObjects(args []js.Value) (any, error) {
return objects, nil return objects, nil
} }
// changeCompositeObjects 复用库层内部选区变换与单次撤销 // changeCompositeObjects 提交内部选区操作,结构变化后返回新的选区路径
// 入参: args 页面索引、父路径、成员路径列表、操作及参数 // 入参: args 页面索引、父路径、成员路径列表、操作及参数
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func changeCompositeObjects(args []js.Value) (any, error) { func changeCompositeObjects(args []js.Value) (any, error) {
return changeObjects(func() error { var selected []int
result, err := changeObjects(func() error {
key := args[1].String() key := args[1].String()
path, err := compositePath(key) path, err := compositePath(key)
if err != nil { if err != nil {
@@ -957,6 +958,23 @@ func changeCompositeObjects(args []js.Value) (any, error) {
page := args[0].Int() page := args[0].Int()
operation := args[3].String() operation := args[3].String()
switch operation { switch operation {
case "delete":
return currentEditor.DeleteCompositeObjects(page, path, indexes)
case "copy":
selected, err = currentEditor.CopyCompositeObjects(page, path, indexes, args[4].Float(), args[5].Float())
return err
case "order":
selected, err = currentEditor.OrderCompositeObjects(page, path, indexes, args[4].String())
return err
case "image":
if len(indexes) != 1 {
return fmt.Errorf("image replacement requires one member")
}
data, err := bytesFromJS(args[4])
if err != nil {
return err
}
return currentEditor.ReplaceCompositeImage(page, path, indexes[0], data)
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":
@@ -1007,6 +1025,17 @@ func changeCompositeObjects(args []js.Value) (any, error) {
return fmt.Errorf("unsupported composite operation") return fmt.Errorf("unsupported composite operation")
} }
}) })
if err != nil {
return nil, err
}
if selected == nil {
return result, nil
}
doc := editorSelectionInfo{editorInfo: result.(editorInfo)}
for _, index := range selected {
doc.SelectedIDs = append(doc.SelectedIDs, fmt.Sprintf("%s/%d", args[1].String(), index))
}
return doc, nil
} }
// editorFont 读取对象实际使用的内嵌字体,供画布输入使用 // editorFont 读取对象实际使用的内嵌字体,供画布输入使用
+13 -7
View File
@@ -219,6 +219,17 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
for _, index := range indexes { for _, index := range indexes {
selected[slices.Index(siblings, index.index)] = true selected[slices.Index(siblings, index.index)] = true
} }
orderEditorObjects(objects, selected, order)
for i, index := range siblings {
layers[indexes[0].layer].Objects[index] = objects[i]
}
e.replaceLayers(page, layers)
return nil
}
// orderEditorObjects 调整选区层级,保持选中及未选中对象各自的相对顺序
// 入参: objects 同容器对象, selected 选中状态, order 排序方向
func orderEditorObjects[T any](objects []T, selected []bool, order string) {
switch order { switch order {
case "up": case "up":
for i := len(objects) - 2; i >= 0; i-- { for i := len(objects) - 2; i >= 0; i-- {
@@ -235,7 +246,7 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
} }
} }
default: default:
ordered := make([]GraphicObject, 0, len(objects)) ordered := make([]T, 0, len(objects))
for _, takeSelected := range []bool{order == "bottom", order != "bottom"} { for _, takeSelected := range []bool{order == "bottom", order != "bottom"} {
for i, object := range objects { for i, object := range objects {
if selected[i] == takeSelected { if selected[i] == takeSelected {
@@ -243,13 +254,8 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
} }
} }
} }
objects = ordered copy(objects, ordered)
} }
for i, index := range siblings {
layers[indexes[0].layer].Objects[index] = objects[i]
}
e.replaceLayers(page, layers)
return nil
} }
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序 // DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序
+40 -34
View File
@@ -34,12 +34,14 @@ type ObjectPath struct {
// CompositeMember 复合对象的直接成员快照,Object包含有效样式,不用于整体替换原对象 // CompositeMember 复合对象的直接成员快照,Object包含有效样式,不用于整体替换原对象
// Bounds与Contours使用页面毫米坐标,StrokeScale为对象描边到页面的倍率 // Bounds与Contours使用页面毫米坐标,StrokeScale为对象描边到页面的倍率
// Position表示当前范围内的直接容器位置,结构操作后需重新枚举
type CompositeMember struct { type CompositeMember struct {
Object GraphicObject Object GraphicObject
Bounds Box Bounds Box
Contours []ObjectContour Contours []ObjectContour
StrokeScale float64 StrokeScale float64
Capabilities ObjectCapabilities Capabilities ObjectCapabilities
Position ObjectPosition
Transform bool Transform bool
} }
@@ -59,6 +61,9 @@ type editorCompositeNode struct {
expanded bool expanded bool
changed bool changed bool
span *editorXML span *editorXML
owner *editorCompositeNode
patches []editorXMLPatch
index int
} }
// CompositeObjects 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级 // CompositeObjects 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级
@@ -189,6 +194,9 @@ func (e *Editor) compositeScope(page int, path ObjectPath) (*Reader, *Renderer,
break break
} }
if depth == len(path.Children) { if depth == len(path.Children) {
for i, member := range members {
member.index = i
}
return reader, renderer, root, members, nil return reader, renderer, root, members, nil
} }
i := path.Children[depth] i := path.Children[depth]
@@ -300,6 +308,7 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
} }
item.parent, item.boundaryInCTM, item.defaults, item.clip, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, visible, child item.parent, item.boundaryInCTM, item.defaults, item.clip, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, visible, child
item.drawParams = drawParams item.drawParams = drawParams
item.owner = n
n.children = append(n.children, item) n.children = append(n.children, item)
} }
} }
@@ -332,6 +341,18 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
// 返回: []CompositeMember 独立快照及操作能力 // 返回: []CompositeMember 独立快照及操作能力
func (e *Editor) 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))
positions := make(map[*editorXML]ObjectPosition)
orderable := make(map[*editorXML]bool)
for _, node := range nodes {
parent := node.span.parent
position := positions[parent]
if position.Count == 0 {
position.Container = fmt.Sprint(node.index)
orderable[parent] = editorContainerOrderable(parent)
}
position.Count++
positions[parent] = position
}
for i, node := range nodes { for i, node := range nodes {
bounds := &boundsRenderer{collect: true} bounds := &boundsRenderer{collect: true}
object := node.object object := node.object
@@ -346,7 +367,8 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
_, 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
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} 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) style, err := e.compositeMemberStyle(node)
if !transform { if !transform {
capability.ReasonCode, capability.Reason = EditUnsupportedContainer, "composite member cannot be transformed" capability.ReasonCode, capability.Reason = EditUnsupportedContainer, "composite member cannot be transformed"
@@ -360,7 +382,10 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
} }
_, ctm := editorGeometry(node.object) _, ctm := editorGeometry(node.object)
matrix := node.parent.Multiply(NewMatrix(ctm)) 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} position := positions[node.span.parent]
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, Position: position, Transform: transform}
position.Index++
positions[node.span.parent] = position
} }
return result return result
} }
@@ -386,15 +411,12 @@ func (e *Editor) compositeMemberStyle(node *editorCompositeNode) (GraphicObject,
} }
// StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽 // StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽
// 不重排文字,不改写继承参数;暂不支持虚线及端点样式 // 不重排文字,不改写继承参数;显式实线覆盖原虚线继承
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米 // 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) 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 { 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 paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil || style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
for i, node := range nodes { for i, node := range nodes {
capability := members[i].Capabilities capability := members[i].Capabilities
if !capability.Transform || paint && !capability.Paint { if !capability.Transform || paint && !capability.Paint {
@@ -417,18 +439,9 @@ func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int,
if err != nil { if err != nil {
return err return err
} }
data, err := editorXMLObject(node.data, node.node, node.object, object) if err := node.update(object); err != nil {
if err != nil {
return err 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 return nil
}) })
@@ -548,6 +561,14 @@ func (e *Editor) editCompositeObjects(page int, path ObjectPath, indexes []int,
ready := e.source.idsReady ready := e.source.idsReady
defer func() { defer func() {
if err != nil { if err != nil {
removed := make(map[string]bool)
for _, resource := range e.resources[count:] {
if resource.image != nil {
removed[resource.image.ID] = true
delete(e.images, resource.image.ID)
}
}
maps.DeleteFunc(e.resourceID, func(_ editorResourceKey, id string) bool { return removed[id] })
clear(e.resources[count:]) clear(e.resources[count:])
e.resources, e.maxID = e.resources[:count], maximum e.resources, e.maxID = e.resources[:count], maximum
e.source.idsReady = ready e.source.idsReady = ready
@@ -698,7 +719,7 @@ func compositeBoundary(object GraphicObject, parent Matrix, inward bool) Graphic
// 入参: node 实例节点 // 入参: node 实例节点
// 返回: []byte 新片段, bool 是否变化, error 错误信息 // 返回: []byte 新片段, bool 是否变化, error 错误信息
func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, error) { func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, error) {
var patches []editorXMLPatch patches := slices.Clone(node.patches)
for _, child := range node.children { for _, child := range node.children {
data, changed, err := e.writeCompositeNode(child) data, changed, err := e.writeCompositeNode(child)
if err != nil { if err != nil {
@@ -748,22 +769,7 @@ func (e *Editor) addCompositeResource(data []byte) (string, error) {
return "", fmt.Errorf("composite contains unsupported identifiers") return "", fmt.Errorf("composite contains unsupported identifiers")
} }
ids := make(map[string]string) ids := make(map[string]string)
var collect func(*editorXML) error if err := e.collectCompositeIDs(root, ids); err != nil {
collect = func(node *editorXML) error {
if id := editorResourceID(node.attr("ID")); id != "" {
if ids[id] != "" {
return fmt.Errorf("duplicate object ID %q", id)
}
ids[id] = e.nextID()
}
for _, child := range node.children {
if err := collect(child); err != nil {
return err
}
}
return nil
}
if err := collect(root); err != nil {
return "", err return "", err
} }
id := ids[editorResourceID(root.attr("ID"))] id := ids[editorResourceID(root.attr("ID"))]
+187
View File
@@ -0,0 +1,187 @@
// 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"
"fmt"
"slices"
)
// siblings 获取同一原始容器内的直接绘制成员,不跨越资源和页块
// 返回: []*editorCompositeNode 按绘制顺序排列的成员
func (n *editorCompositeNode) siblings() []*editorCompositeNode {
var result []*editorCompositeNode
for _, member := range n.owner.children {
if member.span.parent == n.span.parent {
result = append(result, member)
}
}
return result
}
// DeleteCompositeObjects 删除内部成员,保留容器、未选对象及其他实例,一次撤销恢复全部
// 入参: page 页面索引, path 父复合路径, indexes 成员序号
// 返回: error 错误信息
func (e *Editor) DeleteCompositeObjects(page int, path ObjectPath, indexes []int) error {
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error {
for _, node := range nodes {
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, nil})
}
return nil
})
}
// CopyCompositeObjects 在原容器末尾复制选区并按页面毫米平移,保留原文并重映射副本内部标识
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号, dx、dy 页面位移
// 返回: []int 按绘制顺序排列的副本序号, error 错误信息
func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int, dx, dy float64) ([]int, error) {
if !finite(dx) || !finite(dy) {
return nil, fmt.Errorf("copy requires finite offsets")
}
var result []int
err := e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
container := nodes[0].span.parent
for i, node := range nodes {
if !members[i].Capabilities.Copy || node.span.parent != container {
return fmt.Errorf("copy requires editable members in the same container")
}
}
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
ids := make(map[string]string)
for _, node := range nodes {
if err := e.collectCompositeIDs(node.node, ids); err != nil {
return err
}
}
var data []byte
for _, node := range nodes {
copy := *node
if err := e.transformCompositeMember(renderer, &copy, TranslationMatrix(dx, dy)); err != nil {
return err
}
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
if err != nil {
return err
}
data = append(data, fragment...)
}
owner := nodes[0].owner
owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, data})
next := 0
for _, member := range owner.children {
if container.start < member.span.start && member.span.end <= container.close {
next = member.index + 1
}
}
for i := range nodes {
result = append(result, next+i)
}
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
// OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, order 为up、down、top或bottom
// 返回: []int 调整后的选区序号, error 错误信息
func (e *Editor) OrderCompositeObjects(page int, path ObjectPath, indexes []int, order string) ([]int, error) {
if !slices.Contains([]string{"up", "down", "top", "bottom"}, order) {
return nil, fmt.Errorf("invalid object order %q", order)
}
var result []int
err := e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
siblings := nodes[0].siblings()
selected := make([]bool, len(siblings))
for j, node := range nodes {
i := slices.Index(siblings, node)
if i < 0 || !members[j].Capabilities.Order {
return fmt.Errorf("ordering requires members in the same container")
}
selected[i] = true
}
ordered := slices.Clone(siblings)
orderEditorObjects(ordered, selected, order)
for i, node := range siblings {
if node != ordered[i] {
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, ordered[i].data})
}
if slices.Contains(nodes, ordered[i]) {
result = append(result, node.index)
}
}
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
// ReplaceCompositeImage 替换内部图片数据,保留变换、裁剪、蒙版、边框和透明度
// 入参: page 页面索引, path 父复合路径, index 图片序号, data PNG或JPEG数据
// 返回: error 错误信息
func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, data []byte) error {
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error {
node := nodes[0]
if node.object.Type != "ImageObject" {
return fmt.Errorf("composite member is not an image")
}
object := node.object
id, err := e.AddImage(data)
if err != nil {
return err
}
object.ImageObject.ResourceID = id
return node.update(object)
})
}
// update 合并成员属性变化,不重写原文中未修改的字段
// 入参: object 修改后的对象
// 返回: error 错误信息
func (n *editorCompositeNode) update(object GraphicObject) error {
data, err := editorXMLObject(n.data, n.node, n.object, object)
if err != nil || bytes.Equal(data, n.data) {
return err
}
next, err := newEditorCompositeNode(data)
if err != nil {
return err
}
n.data, n.node, n.object, n.changed = data, next.node, next.object, true
return nil
}
// collectCompositeIDs 为已校验副本中的标准标识分配新值,拒绝重复标识
// 入参: node 原节点, ids 原标识与新标识
// 返回: error 错误信息
func (e *Editor) collectCompositeIDs(node *editorXML, ids map[string]string) error {
if id := editorResourceID(node.attr("ID")); id != "" {
if ids[id] != "" {
return fmt.Errorf("duplicate object ID %q", id)
}
ids[id] = e.nextID()
}
for _, child := range node.children {
if err := e.collectCompositeIDs(child, ids); err != nil {
return err
}
}
return nil
}
+5
View File
@@ -62,11 +62,13 @@ type editorSourcePage struct {
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块 // Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复 // Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字 // Paint表示可独立修改纯色填充和描边,不要求重新排版文字
// ReplaceImage表示可替换图片数据,内部成员保留原布局,不开放裁剪或重新适应
// MissingGlyphs提供缺字导致操作受限时的结构化诊断 // MissingGlyphs提供缺字导致操作受限时的结构化诊断
type ObjectCapabilities struct { type ObjectCapabilities struct {
Update bool Update bool
Paint bool Paint bool
ReplaceFont bool ReplaceFont bool
ReplaceImage bool
Reflow bool Reflow bool
LayoutKnown bool LayoutKnown bool
Transform bool Transform bool
@@ -355,6 +357,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
all := ObjectCapabilities{Update: true, Paint: object.Type == "TextObject" || object.Type == "PathObject", ReplaceFont: object.Type == "TextObject", Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true} all := ObjectCapabilities{Update: true, Paint: object.Type == "TextObject" || object.Type == "PathObject", ReplaceFont: object.Type == "TextObject", Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true}
origin := e.objectOrigin(id) origin := e.objectOrigin(id)
if origin == nil { if origin == nil {
all.ReplaceImage = object.Type == "ImageObject"
return all, nil return all, nil
} }
node := origin.node node := origin.node
@@ -384,6 +387,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
errors.As(err, &all.MissingGlyphs) errors.As(err, &all.MissingGlyphs)
} }
all.Paint = all.Paint && e.editorPaintable(object) all.Paint = all.Paint && e.editorPaintable(object)
all.ReplaceImage = all.Update && object.Type == "ImageObject"
if object.Type == "ImageObject" && object.ImageObject.Border != nil && len(object.ImageObject.Actions) != 0 { if object.Type == "ImageObject" && object.ImageObject.Border != nil && len(object.ImageObject.Actions) != 0 {
all.Transform, all.Arrange = false, false all.Transform, all.Arrange = false, false
all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject
@@ -425,6 +429,7 @@ func editorPreservedObject(before, after GraphicObject) bool {
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join
a.dashPatternSet = b.dashPatternSet
case "ImageObject": case "ImageObject":
a, b := &before.ImageObject, after.ImageObject a, b := &before.ImageObject, after.ImageObject
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
+4 -1
View File
@@ -90,11 +90,12 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
if object.Type == "PathObject" { if object.Type == "PathObject" {
obj := &object.PathObject obj := &object.PathObject
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, Join: obj.Join, Cap: obj.Cap, style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, Join: obj.Join, Cap: obj.Cap,
MiterLimit: obj.MiterLimit, DashPattern: obj.DashPattern, DashOffset: obj.DashOffset, MiterLimit: obj.MiterLimit, DashPattern: obj.DashPattern, dashPatternSet: obj.dashPatternSet, DashOffset: obj.DashOffset,
FillColor: obj.FillColor, StrokeColor: obj.StrokeColor}) FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
obj.DrawParam = "" obj.DrawParam = ""
obj.LineWidth, obj.Join, obj.Cap = style.LineWidth, style.Join, style.Cap obj.LineWidth, obj.Join, obj.Cap = style.LineWidth, style.Join, style.Cap
obj.MiterLimit, obj.DashPattern, obj.DashOffset = style.MiterLimit, style.DashPattern, style.DashOffset obj.MiterLimit, obj.DashPattern, obj.DashOffset = style.MiterLimit, style.DashPattern, style.DashOffset
obj.dashPatternSet = style.dashPatternSet
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
} else { } else {
obj := &object.TextObject obj := &object.TextObject
@@ -221,6 +222,7 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
} }
if style.DashPattern != nil { if style.DashPattern != nil {
path.DashPattern = *style.DashPattern path.DashPattern = *style.DashPattern
path.dashPatternSet = path.DashPattern == ""
} }
if style.DashOffset != nil { if style.DashOffset != nil {
path.DashOffset = style.DashOffset path.DashOffset = style.DashOffset
@@ -316,6 +318,7 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) error {
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor 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.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
to.DashPattern, to.DashOffset = from.DashPattern, from.DashOffset to.DashPattern, to.DashOffset = from.DashPattern, from.DashOffset
to.dashPatternSet = from.dashPatternSet
scale := editorStrokeScale(from.CTM) / editorStrokeScale(to.CTM) scale := editorStrokeScale(from.CTM) / editorStrokeScale(to.CTM)
if scale != 1 { if scale != 1 {
if to.LineWidth == 0 { if to.LineWidth == 0 {
+10 -3
View File
@@ -104,6 +104,13 @@ func (n *editorXML) attr(name string) string {
return "" return ""
} }
// hasAttr 判断属性是否存在,区分空值与省略
// 入参: name 属性名称
// 返回: bool 是否存在
func (n *editorXML) hasAttr(name string) bool {
return slices.ContainsFunc(n.attrs, func(attr xml.Attr) bool { return attr.Name.Space == "" && attr.Name.Local == name })
}
// editorPatchXML 按位置应用互不重叠的替换 // editorPatchXML 按位置应用互不重叠的替换
// 入参: data 原文, patches 修改区间 // 入参: data 原文, patches 修改区间
// 返回: []byte 修改后的XML // 返回: []byte 修改后的XML
@@ -363,7 +370,7 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
} }
changes := make(map[string]string) changes := make(map[string]string)
for _, attr := range append(oldNode.attrs, newNode.attrs...) { for _, attr := range append(oldNode.attrs, newNode.attrs...) {
if attr.Name.Space == "" && oldNode.attr(attr.Name.Local) != newNode.attr(attr.Name.Local) { if attr.Name.Space == "" && (oldNode.attr(attr.Name.Local) != newNode.attr(attr.Name.Local) || oldNode.hasAttr(attr.Name.Local) != newNode.hasAttr(attr.Name.Local)) {
changes[attr.Name.Local] = newNode.attr(attr.Name.Local) changes[attr.Name.Local] = newNode.attr(attr.Name.Local)
} }
} }
@@ -388,7 +395,7 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
if value, ok := changes[attr.Name.Local]; ok { if value, ok := changes[attr.Name.Local]; ok {
attr.Value = value attr.Value = value
delete(changes, attr.Name.Local) delete(changes, attr.Name.Local)
if value == "" { if !newNode.hasAttr(attr.Name.Local) {
continue continue
} }
} }
@@ -402,7 +409,7 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
result.WriteByte('"') result.WriteByte('"')
} }
for _, attr := range newNode.attrs { for _, attr := range newNode.attrs {
if value, ok := changes[attr.Name.Local]; ok && value != "" { if value, ok := changes[attr.Name.Local]; ok {
result.WriteString(" " + attr.Name.Local + "=\"") result.WriteString(" " + attr.Name.Local + "=\"")
_ = xml.EscapeText(&result, []byte(value)) _ = xml.EscapeText(&result, []byte(value))
result.WriteByte('"') result.WriteByte('"')
+40
View File
@@ -19,6 +19,46 @@ import (
"strconv" "strconv"
) )
// UnmarshalXML 解析路径并区分省略的虚线样式与显式实线
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type plain PathObject
var value struct {
plain
DashPattern *string `xml:"DashPattern,attr"`
}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*p = PathObject(value.plain)
if value.DashPattern != nil {
p.DashPattern = *value.DashPattern
p.dashPatternSet = p.DashPattern == ""
}
return nil
}
// UnmarshalXML 解析绘制参数并保留显式实线对基础参数的覆盖
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *DrawParam) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type plain DrawParam
var value struct {
plain
DashPattern *string `xml:"DashPattern,attr"`
}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*p = DrawParam(value.plain)
if value.DashPattern != nil {
p.DashPattern = *value.DashPattern
p.dashPatternSet = p.DashPattern == ""
}
return nil
}
// UnmarshalXML 解析填充颜色并区分未支持的复杂颜色 // UnmarshalXML 解析填充颜色并区分未支持的复杂颜色
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
+1
View File
@@ -172,6 +172,7 @@ type PathObject struct {
Cap string `xml:"Cap,attr"` Cap string `xml:"Cap,attr"`
DashOffset *float64 `xml:"DashOffset,attr"` DashOffset *float64 `xml:"DashOffset,attr"`
DashPattern string `xml:"DashPattern,attr"` DashPattern string `xml:"DashPattern,attr"`
dashPatternSet bool
MiterLimit float64 `xml:"MiterLimit,attr"` MiterLimit float64 `xml:"MiterLimit,attr"`
CTM string `xml:"CTM,attr"` CTM string `xml:"CTM,attr"`
Alpha *int `xml:"Alpha,attr"` Alpha *int `xml:"Alpha,attr"`
+2 -1
View File
@@ -249,8 +249,9 @@ func mergeDrawParam(base DrawParam, dp *DrawParam) *DrawParam {
if dp.Cap != "" { if dp.Cap != "" {
base.Cap = dp.Cap base.Cap = dp.Cap
} }
if dp.DashPattern != "" { if dp.DashPattern != "" || dp.dashPatternSet {
base.DashPattern = dp.DashPattern base.DashPattern = dp.DashPattern
base.dashPatternSet = dp.dashPatternSet
} }
if dp.DashOffset != nil { if dp.DashOffset != nil {
base.DashOffset = dp.DashOffset base.DashOffset = dp.DashOffset
+2 -2
View File
@@ -126,7 +126,7 @@ func (s *pathStyle) applyDrawParam(r *Renderer, dp *DrawParam, bx, by, pageH flo
s.lineCap = pathLineCap(dp.Cap, s.lineCap) s.lineCap = pathLineCap(dp.Cap, s.lineCap)
} }
s.applyLineJoin(dp.Join, dp.MiterLimit) s.applyLineJoin(dp.Join, dp.MiterLimit)
if dp.DashPattern != "" { if dp.DashPattern != "" || dp.dashPatternSet {
s.dashPattern = parseFloats(dp.DashPattern) s.dashPattern = parseFloats(dp.DashPattern)
} }
if dp.DashOffset != nil { if dp.DashOffset != nil {
@@ -150,7 +150,7 @@ func (s *pathStyle) applyPathObject(r *Renderer, obj PathObject, bx, by, pageH f
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap) s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
} }
s.applyLineJoin(obj.Join, obj.MiterLimit) s.applyLineJoin(obj.Join, obj.MiterLimit)
if obj.DashPattern != "" { if obj.DashPattern != "" || obj.dashPatternSet {
s.dashPattern = parseFloats(obj.DashPattern) s.dashPattern = parseFloats(obj.DashPattern)
} }
if obj.DashOffset != nil { if obj.DashOffset != nil {
+1
View File
@@ -88,6 +88,7 @@ type DrawParam struct {
Cap string `xml:"Cap,attr"` Cap string `xml:"Cap,attr"`
DashOffset *float64 `xml:"DashOffset,attr"` DashOffset *float64 `xml:"DashOffset,attr"`
DashPattern string `xml:"DashPattern,attr"` DashPattern string `xml:"DashPattern,attr"`
dashPatternSet bool
MiterLimit float64 `xml:"MiterLimit,attr"` MiterLimit float64 `xml:"MiterLimit,attr"`
Font string `xml:"Font,attr"` Font string `xml:"Font,attr"`
Size float64 `xml:"Size,attr"` Size float64 `xml:"Size,attr"`
+4
View File
@@ -637,7 +637,11 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
attrs.add("Join", obj.Join) attrs.add("Join", obj.Join)
attrs.add("Cap", obj.Cap) attrs.add("Cap", obj.Cap)
attrs.add("Rule", obj.Rule) attrs.add("Rule", obj.Rule)
if obj.dashPatternSet {
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: obj.DashPattern})
} else {
attrs.add("DashPattern", obj.DashPattern) attrs.add("DashPattern", obj.DashPattern)
}
if obj.DashOffset != nil { if obj.DashOffset != nil {
attrs.add("DashOffset", ofdNumber(*obj.DashOffset)) attrs.add("DashOffset", ofdNumber(*obj.DashOffset))
} }