feat(编辑能力): 完善继承样式编辑与受限提示,优化编辑入口和工具栏布局
build.yaml / build (push) Successful in 37s

This commit is contained in:
xiaoqidun committed 2026-09-16 13:27:05 +08:00
1 parent 237487b53c
commit 0034e0edbd
11 files changed
+234 -52

No files matched your search

+31 -31
View File
@@ -18,6 +18,7 @@
<div class="tool-group"> <div class="tool-group">
<button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开" aria-keyshortcuts="Control+O Meta+O"><span class="open-icon" aria-hidden="true"></span></button> <button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开" aria-keyshortcuts="Control+O Meta+O"><span class="open-icon" aria-hidden="true"></span></button>
<button id="newButton" class="icon-button" type="button" title="新建" aria-label="新建文档"><span class="new-icon" aria-hidden="true"></span></button> <button id="newButton" class="icon-button" type="button" title="新建" aria-label="新建文档"><span class="new-icon" aria-hidden="true"></span></button>
<button id="editButton" class="icon-button" type="button" title="编辑文档" aria-label="编辑文档" aria-pressed="false" disabled><span class="edit-icon" aria-hidden="true"></span></button>
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button> <button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button>
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button> <button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button>
</div> </div>
@@ -43,7 +44,6 @@
<button id="annotationButton" class="button" type="button" data-short="注" title="关闭注解" aria-label="关闭注解" aria-pressed="true">注解</button> <button id="annotationButton" class="button" type="button" data-short="注" title="关闭注解" aria-label="关闭注解" aria-pressed="true">注解</button>
</div> </div>
<div class="tool-group"> <div class="tool-group">
<button id="editButton" class="button" type="button" data-short="编" title="编辑文档" aria-label="编辑文档" aria-pressed="false" disabled>编辑</button>
<select id="exportFormat" class="format-select" title="导出格式" aria-label="导出格式" disabled></select> <select id="exportFormat" class="format-select" title="导出格式" aria-label="导出格式" disabled></select>
<select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled> <select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled>
<option value="96">96</option> <option value="96">96</option>
@@ -91,28 +91,13 @@
</select> </select>
<span>对象</span> <span>对象</span>
</div> </div>
<div class="editor-group editor-transform"> <div class="editor-group">
<select id="objectRotate" class="order-select" aria-label="对象旋转" disabled> <button id="insertTextButton" class="button" type="button" aria-label="添加文字" aria-pressed="false">文字</button>
<option value="" hidden>旋转</option> <button id="insertImageButton" class="button" type="button" aria-label="添加图片">图片</button>
<option value="90">右转90°</option> <button id="drawLineButton" class="button" type="button" aria-label="绘制直线" aria-pressed="false">直线</button>
<option value="-90">左转90°</option> <button id="drawRectangleButton" class="button" type="button" aria-label="绘制矩形" aria-pressed="false">矩形</button>
</select> <button id="drawEllipseButton" class="button" type="button" aria-label="绘制椭圆" aria-pressed="false">椭圆</button>
<select id="objectFlip" class="order-select" aria-label="对象翻转" disabled> <span>插入</span>
<option value="" hidden>翻转</option>
<option value="horizontal">水平</option>
<option value="vertical">垂直</option>
</select>
<span>变换</span>
</div>
<div class="editor-group editor-image">
<select id="imageFit" aria-label="图片适应方式" disabled>
<option value="" hidden>缩放</option>
<option value="contain">适应</option>
<option value="cover">填充</option>
</select>
<button id="cropImageButton" class="button" type="button" aria-label="裁剪图片" aria-pressed="false" disabled>裁剪</button>
<button id="resetCropButton" class="button" type="button" aria-label="还原图片" disabled>还原</button>
<span>图片</span>
</div> </div>
<div class="editor-group"> <div class="editor-group">
<div class="text-font"><select id="textFont" aria-label="字体" title="字体" disabled></select><button id="textFontAdd" class="icon-button" type="button" title="添加字体" aria-label="添加字体">+</button></div> <div class="text-font"><select id="textFont" aria-label="字体" title="字体" disabled></select><button id="textFontAdd" class="icon-button" type="button" title="添加字体" aria-label="添加字体">+</button></div>
@@ -133,20 +118,35 @@
<label class="editor-field"><span>字距</span><span class="editor-number"><input id="textSpacing" type="number" step="any" placeholder="0" aria-label="字距" title="字距" disabled><span>pt</span></span></label> <label class="editor-field"><span>字距</span><span class="editor-number"><input id="textSpacing" type="number" step="any" placeholder="0" aria-label="字距" title="字距" disabled><span>pt</span></span></label>
<span>段落</span> <span>段落</span>
</div> </div>
<div class="editor-group">
<button id="insertTextButton" class="button" type="button" aria-label="添加文字" aria-pressed="false">文字</button>
<button id="insertImageButton" class="button" type="button" aria-label="添加图片">图片</button>
<button id="drawLineButton" class="button" type="button" aria-label="绘制直线" aria-pressed="false">直线</button>
<button id="drawRectangleButton" class="button" type="button" aria-label="绘制矩形" aria-pressed="false">矩形</button>
<button id="drawEllipseButton" class="button" type="button" aria-label="绘制椭圆" aria-pressed="false">椭圆</button>
<span>插入</span>
</div>
<div class="editor-group"> <div class="editor-group">
<div class="shape-paint"><label><input id="shapeFill" type="checkbox">填充</label><input id="shapeFillColor" class="color-input" type="color" value="#dcefeb" aria-label="填充颜色" title="填充颜色"></div> <div class="shape-paint"><label><input id="shapeFill" type="checkbox">填充</label><input id="shapeFillColor" class="color-input" type="color" value="#dcefeb" aria-label="填充颜色" title="填充颜色"></div>
<div class="shape-paint"><label><input id="shapeStroke" type="checkbox" checked>描边</label><input id="shapeStrokeColor" class="color-input" type="color" value="#000000" aria-label="描边颜色" title="描边颜色"></div> <div class="shape-paint"><label><input id="shapeStroke" type="checkbox" checked>描边</label><input id="shapeStrokeColor" class="color-input" type="color" value="#000000" aria-label="描边颜色" title="描边颜色"></div>
<label class="editor-field"><span>线宽</span><span class="editor-number"><input id="shapeWidth" type="number" value="1" min="0" step="any" required aria-label="线宽" title="线宽"><span>pt</span></span></label> <label class="editor-field"><span>线宽</span><span class="editor-number"><input id="shapeWidth" type="number" value="1" min="0" step="any" required aria-label="线宽" title="线宽"><span>pt</span></span></label>
<span>图形</span> <span>图形</span>
</div> </div>
<div class="editor-group editor-image">
<select id="imageFit" aria-label="图片适应方式" disabled>
<option value="" hidden>缩放</option>
<option value="contain">适应</option>
<option value="cover">填充</option>
</select>
<button id="cropImageButton" class="button" type="button" aria-label="裁剪图片" aria-pressed="false" disabled>裁剪</button>
<button id="resetCropButton" class="button" type="button" aria-label="还原图片" disabled>还原</button>
<span>图片</span>
</div>
<div class="editor-group editor-transform">
<select id="objectRotate" class="order-select" aria-label="对象旋转" disabled>
<option value="" hidden>旋转</option>
<option value="90">右转90°</option>
<option value="-90">左转90°</option>
</select>
<select id="objectFlip" class="order-select" aria-label="对象翻转" disabled>
<option value="" hidden>翻转</option>
<option value="horizontal">水平</option>
<option value="vertical">垂直</option>
</select>
<span>变换</span>
</div>
<div class="editor-group"> <div class="editor-group">
<button id="addPageButton" class="button" type="button" aria-label="新增页面">新增</button> <button id="addPageButton" class="button" type="button" aria-label="新增页面">新增</button>
<button id="copyPageButton" class="button" type="button" aria-label="复制页面">复制</button> <button id="copyPageButton" class="button" type="button" aria-label="复制页面">复制</button>
+32
View File
@@ -374,6 +374,34 @@ button:not(:disabled):hover {
transform: translate(-50%, -50%) rotate(90deg); transform: translate(-50%, -50%) rotate(90deg);
} }
.edit-icon {
position: relative;
width: 15px;
height: 15px;
transform: rotate(-45deg);
}
.edit-icon::before {
position: absolute;
top: 5px;
left: 4px;
width: 9px;
height: 3px;
border: 1px solid currentColor;
content: "";
}
.edit-icon::after {
position: absolute;
top: 5px;
left: 0;
width: 4px;
height: 5px;
background: currentColor;
clip-path: polygon(0 50%, 100% 0, 100% 100%);
content: "";
}
.toolbar-space { .toolbar-space {
flex: 1 1 auto; flex: 1 1 auto;
} }
@@ -1305,6 +1333,10 @@ button.outline-link:hover {
outline: calc(1px / var(--surface-scale)) solid var(--accent); outline: calc(1px / var(--surface-scale)) solid var(--accent);
} }
.edit-object.read-only {
cursor: default;
}
.edit-line::before { .edit-line::before {
position: absolute; position: absolute;
inset: calc(-4px / var(--surface-scale)); inset: calc(-4px / var(--surface-scale));
+9 -2
View File
@@ -1,4 +1,4 @@
import { CanvasEditor, canEditObject } from "./ofdgo_edit.js"; import { CanvasEditor, canEditObject, objectEditReason } from "./ofdgo_edit.js";
import { FontManager } from "./ofdgo_font.js"; import { FontManager } from "./ofdgo_font.js";
const MM_TO_PX = 96 / 25.4; const MM_TO_PX = 96 / 25.4;
@@ -250,7 +250,14 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
busy: () => document.body.hasAttribute("aria-busy"), busy: () => document.body.hasAttribute("aria-busy"),
rotation: () => state.rotation, rotation: () => state.rotation,
canInsert: (index) => pageCan("insert", index), canInsert: (index) => pageCan("insert", index),
onSelect: updateObjectControls, onSelect: (item, previous) => {
updateObjectControls(item);
const reason = objectEditReason(item), previousReason = objectEditReason(previous);
if (state.editing && state.doc && !document.body.hasAttribute("aria-busy")
&& (reason || previousReason && el.statusText.textContent === previousReason)) {
setStatus(reason || pageStatus(state.pageIndex, state.doc.pageCount));
}
},
onTransform: (item, change) => changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item, onTransform: (item, change) => changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item,
change.x + item.x * (1 - change.scale), change.y + item.y * (1 - change.scale), change.scale), change.x + item.x * (1 - change.scale), change.y + item.y * (1 - change.scale), change.scale),
onReshape: (item, box) => changeDocument("ofdgoReshapeObject", item, box.x, box.y, box.width, box.height), onReshape: (item, box) => changeDocument("ofdgoReshapeObject", item, box.x, box.y, box.width, box.height),
+20 -1
View File
@@ -122,6 +122,22 @@ export function canEditObject(item, capability) {
return Boolean(item) && (item.items || [item]).every(object => Boolean(object.capabilities?.[capability])); return Boolean(item) && (item.items || [item]).every(object => Boolean(object.capabilities?.[capability]));
} }
export function objectEditReason(item) {
const reasons = (item?.items || (item ? [item] : [])).map(object => {
const reason = object.capabilities?.reason || "";
if (!reason) return "";
let message = "对象特性暂不支持";
if (reason.includes("embedded font")) message = "缺少内嵌字体";
else if (reason.includes("does not contain")) message = "字体缺少所需文字";
else if (reason.includes("color") || reason.includes("RGB")) message = "颜色样式暂不支持";
else if (reason.includes("draw parameter")) message = "绘制参数暂不支持";
else if (reason.includes("layer")) message = "图层特性暂不支持";
else if (reason.includes("invalid")) message = "对象数据异常";
return `${object.capabilities.transform ? "部分操作受限" : "暂不可编辑"}:${message}`;
});
return [...new Set(reasons.filter(Boolean))].join(";");
}
export class CanvasEditor { export class CanvasEditor {
constructor(viewer, options) { constructor(viewer, options) {
this.viewer = viewer; this.viewer = viewer;
@@ -182,6 +198,8 @@ export class CanvasEditor {
for (const object of page.objects) { for (const object of page.objects) {
const node = document.createElement("div"); const node = document.createElement("div");
node.className = "edit-object"; node.className = "edit-object";
node.classList.toggle("read-only", !canEditObject(object, "transform"));
node.title = objectEditReason(object);
if (object.shape === "line") { if (object.shape === "line") {
node.classList.add("edit-line"); node.classList.add("edit-line");
} }
@@ -269,6 +287,7 @@ export class CanvasEditor {
} }
setSelection(items) { setSelection(items) {
const previous = this.selected;
if (this.crop && (items.length !== 1 || items[0] !== this.crop.item)) this.closeCrop(); if (this.crop && (items.length !== 1 || items[0] !== this.crop.item)) this.closeCrop();
if (this.selected?.items) this.selected.node.remove(); if (this.selected?.items) this.selected.node.remove();
for (const item of this.items()) { for (const item of this.items()) {
@@ -296,7 +315,7 @@ export class CanvasEditor {
item.node.classList.toggle("multi-selected", items.length > 1); item.node.classList.toggle("multi-selected", items.length > 1);
item.node.setAttribute("aria-pressed", "true"); item.node.setAttribute("aria-pressed", "true");
} }
this.options.onSelect(this.selected); this.options.onSelect(this.selected, previous);
} }
select(item, add = false) { select(item, add = false) {
+5 -9
View File
@@ -607,7 +607,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
if currentEditor == nil { if currentEditor == nil {
return objects, nil return objects, nil
} }
page, err := currentSession.pageContent(index) page, err := currentEditor.Page(index)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -651,7 +651,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !capability.Transform && !capability.Update { if capability.Reason == "nested objects are read-only" {
continue continue
} }
var box ofdgo.Box var box ofdgo.Box
@@ -664,12 +664,12 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
box, err = ofdgo.ParseBox(object.PathObject.Boundary) box, err = ofdgo.ParseBox(object.PathObject.Boundary)
} }
if err != nil { if err != nil {
return nil, err continue
} }
if box.W > 0 && box.H > 0 { if box.W > 0 && box.H > 0 {
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects), "layer": layer.ID, item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects), "layer": layer.ID,
"capabilities": map[string]any{"update": capability.Update, "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}} "capabilities": map[string]any{"update": capability.Update, "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}}
if object.Type == "ImageObject" { if object.Type == "ImageObject" && capability.Update {
full, err := object.ImageObject.ImageBounds() full, err := object.ImageObject.ImageBounds()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -689,11 +689,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
item["lineWidth"] = path.LineWidth * editorPathScale(path) item["lineWidth"] = path.LineWidth * editorPathScale(path)
} }
if object.Type == "TextObject" { if object.Type == "TextObject" {
source, err := currentEditor.Object(index, id) value, layout := object.TextObject.TextLayout()
if err != nil {
return nil, err
}
value, layout := source.TextObject.TextLayout()
item["text"], item["font"], item["fontName"], item["size"] = value, object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size item["text"], item["font"], item["fontName"], item["size"] = value, object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size
item["wrap"], item["align"], item["paragraphHeight"] = layout.Wrap, layout.Align, layout.LineHeight item["wrap"], item["align"], item["paragraphHeight"] = layout.Wrap, layout.Align, layout.LineHeight
item["letterSpacing"] = layout.LetterSpacing item["letterSpacing"] = layout.LetterSpacing
+2 -1
View File
@@ -392,7 +392,8 @@ func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
return ids[0], nil return ids[0], nil
} }
// Object 获取对象的独立副本,修改副本不影响文档 // Object 获取对象的独立副本,修改副本不影响文档。
// 可编辑的继承样式解析为直接属性;仅在对象修改或复制时写入独立样式。
// 入参: page 页面索引, id 对象标识 // 入参: page 页面索引, id 对象标识
// 返回: GraphicObject 对象内容, error 错误信息 // 返回: GraphicObject 对象内容, error 错误信息
func (e *Editor) Object(page int, id string) (GraphicObject, error) { func (e *Editor) Object(page int, id string) (GraphicObject, error) {
+6
View File
@@ -316,6 +316,12 @@ func (e *Editor) updateObjects(page int, objects []GraphicObject, geometry bool)
if err != nil { if err != nil {
return err return err
} }
if e.originalPage(page) {
after[i], err = e.resolveEditorStyle(after[i], e.pages[page].Content.Layer[indexes[i].layer].DrawParam)
if err != nil {
return err
}
}
} }
} }
if reflect.DeepEqual(before, after) { if reflect.DeepEqual(before, after) {
+19 -5
View File
@@ -44,6 +44,7 @@ type editorObjectOrigin struct {
page *editorSourcePage page *editorSourcePage
node *editorXML node *editorXML
object GraphicObject object GraphicObject
reason string
} }
// editorSourcePage 保存页面原文、解析快照及可定位的直接对象。 // editorSourcePage 保存页面原文、解析快照及可定位的直接对象。
@@ -292,15 +293,25 @@ func (e *Editor) loadSourcePage(index int) error {
} }
} }
source.data, source.root, source.nodes = data, root, nodes source.data, source.root, source.nodes = data, root, nodes
source.original = page for i := range page.Content.Layer {
for _, layer := range page.Content.Layer { layer := &page.Content.Layer[i]
for _, object := range layer.Objects { for j, object := range layer.Objects {
id := editorObjectID(object) id := editorObjectID(object)
if node := nodes[id]; node != nil { if node := nodes[id]; node != nil {
e.source.origins[id] = &editorObjectOrigin{page: source, node: node, object: object} origin := &editorObjectOrigin{page: source, node: node, object: object}
e.source.origins[id] = origin
if editorXMLSupported(node) {
resolved, err := e.resolveEditorStyle(object, layer.DrawParam)
if err != nil {
origin.reason = err.Error()
} else {
layer.Objects[j] = resolved
}
}
} }
} }
} }
source.original = page
e.pages[index] = copyEditorPage(*page) e.pages[index] = copyEditorPage(*page)
return nil return nil
} }
@@ -332,9 +343,12 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
} }
return all, nil return all, nil
} }
if layer.DrawParam != "" || !editorXMLSupported(node) { if !editorXMLSupported(node) {
return ObjectCapabilities{Reason: "object uses unsupported editing features"}, nil return ObjectCapabilities{Reason: "object uses unsupported editing features"}, nil
} }
if origin := e.objectOrigin(id); origin.reason != "" {
return ObjectCapabilities{Reason: origin.reason}, nil
}
for _, attr := range node.parent.attrs { for _, attr := range node.parent.attrs {
if attr.Name.Space == "xmlns" || attr.Name.Space == "" && attr.Name.Local == "xmlns" { if attr.Name.Space == "xmlns" || attr.Name.Space == "" && attr.Name.Local == "xmlns" {
continue continue
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import "fmt"
// editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照。
// 入参: id 绘制参数标识, visited 已访问标识
// 返回: *DrawParam 合并参数, error 错误信息
func (e *Editor) editorDrawParam(id string, visited map[string]bool) (*DrawParam, error) {
if id == "" {
return &DrawParam{}, nil
}
dp := e.source.reader.drawParamCache[id]
if dp == nil || visited[id] {
return nil, fmt.Errorf("invalid draw parameter reference %q", id)
}
visited[id] = true
base, err := e.editorDrawParam(dp.Relative, visited)
if err != nil {
return nil, err
}
return mergeDrawParam(*base, dp), nil
}
// resolveEditorStyle 为直接对象生成独立的有效样式,不改写共享资源或原XML。
// 入参: object 原对象, layer 图层绘制参数标识
// 返回: GraphicObject 编辑快照, error 错误信息
func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (GraphicObject, error) {
var id string
switch object.Type {
case "TextObject":
id = object.TextObject.DrawParam
case "PathObject":
id = object.PathObject.DrawParam
default:
return object, nil
}
if layer == "" && id == "" {
return object, nil
}
base, err := e.editorDrawParam(layer, make(map[string]bool))
if err != nil {
return GraphicObject{}, err
}
dp, err := e.editorDrawParam(id, make(map[string]bool))
if err != nil {
return GraphicObject{}, err
}
style := mergeDrawParam(*base, dp)
if object.Type == "PathObject" {
obj := &object.PathObject
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, Join: obj.Join, Cap: obj.Cap,
MiterLimit: obj.MiterLimit, DashPattern: obj.DashPattern, DashOffset: obj.DashOffset,
FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
obj.DrawParam = ""
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.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
} else {
obj := &object.TextObject
if style.Cap != "" && style.Cap != "Butt" || style.DashPattern != "" || style.DashOffset != nil && *style.DashOffset != 0 {
return GraphicObject{}, fmt.Errorf("text draw parameters require unsupported stroke styles")
}
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, Join: obj.Join,
MiterLimit: obj.MiterLimit, FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
text := mergeDrawParam(*dp, &DrawParam{Font: obj.Font, Size: obj.Size, Weight: obj.Weight, Italic: obj.Italic})
obj.DrawParam = ""
obj.Font, obj.Size, obj.Weight, obj.Italic = text.Font, text.Size, text.Weight, text.Italic
obj.LineWidth, obj.Join, obj.MiterLimit = style.LineWidth, style.Join, style.MiterLimit
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
}
for _, color := range []*FillColor{style.FillColor, (*FillColor)(style.StrokeColor)} {
if err := creationColor(color); err != nil {
return GraphicObject{}, fmt.Errorf("unsupported draw parameter color: %w", err)
}
}
return cloneEditorObject(object)
}
+17 -3
View File
@@ -191,17 +191,17 @@ func editorXMLSetText(data []byte, node *editorXML, values [][2]string) []byte {
// 入参: node 对象或子节点 // 入参: node 对象或子节点
// 返回: bool 是否支持完整编辑 // 返回: bool 是否支持完整编辑
func editorXMLSupported(node *editorXML) bool { func editorXMLSupported(node *editorXML) bool {
if node.name.Space != "" && node.name.Space != ofdNamespace { if node.name.Space != "" && node.name.Space != ofdNamespace && node.name.Space != "http://www.ofdspec.org" {
return false return false
} }
var allowed, children string var allowed, children string
switch node.name.Local { switch node.name.Local {
case "TextObject": case "TextObject":
allowed, children = "ID Boundary CTM Font Size HScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Visible Stroke Fill Alpha", "FillColor StrokeColor TextCode" allowed, children = "ID Boundary CTM DrawParam Font Size HScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Visible Stroke Fill Alpha", "FillColor StrokeColor TextCode"
case "PathObject", "Path": case "PathObject", "Path":
allowed, children = "Boundary CTM LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha", "StrokeColor FillColor AbbreviatedData" allowed, children = "Boundary CTM LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha", "StrokeColor FillColor AbbreviatedData"
if node.name.Local == "PathObject" { if node.name.Local == "PathObject" {
allowed += " ID" allowed += " ID DrawParam"
} }
case "ImageObject": case "ImageObject":
allowed, children = "ID Boundary CTM ResourceID ImageMask Visible Alpha", "Clips" allowed, children = "ID Boundary CTM ResourceID ImageMask Visible Alpha", "Clips"
@@ -236,12 +236,26 @@ func editorXMLSupported(node *editorXML) bool {
} }
// editorXMLObject 修改对象发生变化的属性与内容,保留原文中的显式默认值。 // editorXMLObject 修改对象发生变化的属性与内容,保留原文中的显式默认值。
// 有效样式快照在修改后展开原绘制参数,避免已清除的属性重新继承。
// 入参: data 页面原文, node 原对象节点, before 原对象, after 新对象 // 入参: data 页面原文, node 原对象节点, before 原对象, after 新对象
// 返回: []byte 对象XML, error 错误信息 // 返回: []byte 对象XML, error 错误信息
func editorXMLObject(data []byte, node *editorXML, before, after GraphicObject) ([]byte, error) { func editorXMLObject(data []byte, node *editorXML, before, after GraphicObject) ([]byte, error) {
if before.Type != after.Type { if before.Type != after.Type {
return editorObjectXML(after) return editorObjectXML(after)
} }
if node.attr("DrawParam") != "" && before.TextObject.DrawParam == "" && before.PathObject.DrawParam == "" {
before = GraphicObject{Type: before.Type}
var err error
switch before.Type {
case "TextObject":
err = xml.Unmarshal(data[node.start:node.end], &before.TextObject)
case "PathObject":
err = xml.Unmarshal(data[node.start:node.end], &before.PathObject)
}
if err != nil {
return nil, err
}
}
oldXML, err := editorObjectXML(before) oldXML, err := editorObjectXML(before)
if err != nil { if err != nil {
return nil, err return nil, err
+2
View File
@@ -467,6 +467,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
case "TextObject": case "TextObject":
obj := object.TextObject obj := object.TextObject
attrs.add("ID", obj.ID) attrs.add("ID", obj.ID)
attrs.add("DrawParam", obj.DrawParam)
attrs.add("Boundary", obj.Boundary) attrs.add("Boundary", obj.Boundary)
attrs.add("CTM", obj.CTM) attrs.add("CTM", obj.CTM)
attrs.add("Font", obj.Font) attrs.add("Font", obj.Font)
@@ -489,6 +490,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
case "PathObject", "Path": case "PathObject", "Path":
obj := object.PathObject obj := object.PathObject
attrs.add("ID", obj.ID) attrs.add("ID", obj.ID)
attrs.add("DrawParam", obj.DrawParam)
attrs.add("Boundary", obj.Boundary) attrs.add("Boundary", obj.Boundary)
attrs.add("CTM", obj.CTM) attrs.add("CTM", obj.CTM)
attrs.number("LineWidth", obj.LineWidth) attrs.number("LineWidth", obj.LineWidth)