mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
This commit is contained in:
1 parent
237487b53c
commit
0034e0edbd
11 files changed
+234
-52
No files matched your search
+31
-31
@@ -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">☰</button>
|
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">☰</button>
|
||||||
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">ⓘ</button>
|
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">ⓘ</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>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user