mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +08:00
1 parent
ff06840bcd
commit
ed11ade4e8
11 files changed
+468
-37
No files matched your search
@@ -265,9 +265,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
||||||
<form method="dialog">
|
<form id="annotationForm" method="dialog">
|
||||||
<div id="annotationDetail" class="annotation-detail"></div>
|
<div id="annotationDetail" class="annotation-detail"></div>
|
||||||
<div class="form-actions"><button class="button" type="submit" autofocus>关闭</button></div>
|
<div id="annotationFields" hidden>
|
||||||
|
<div class="form-row"><label for="annotationRemark">说明</label><textarea id="annotationRemark" rows="4"></textarea></div>
|
||||||
|
<div class="form-row"><label for="annotationCreator">作者</label><input id="annotationCreator" type="text"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions"><div id="annotationStatus" class="form-status" role="status"></div><button id="annotationClose" class="button" type="button" autofocus>关闭</button><button id="annotationSubmit" class="button" type="submit" hidden>确定</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
<dialog id="pagePanel" class="form-dialog" aria-label="页面设置">
|
<dialog id="pagePanel" class="form-dialog" aria-label="页面设置">
|
||||||
|
|||||||
@@ -798,7 +798,8 @@ button:not(:disabled):hover {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions > button:only-child {
|
.form-actions > button:only-child,
|
||||||
|
.form-actions > button:has(+ button[hidden]) {
|
||||||
grid-column: -2;
|
grid-column: -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,6 +1298,8 @@ button.outline-link:hover {
|
|||||||
.navigation-tabs[hidden],
|
.navigation-tabs[hidden],
|
||||||
.page-selection-tools[hidden],
|
.page-selection-tools[hidden],
|
||||||
.form-row[hidden],
|
.form-row[hidden],
|
||||||
|
.form-actions > [hidden],
|
||||||
|
.annotation-detail[hidden],
|
||||||
.tool-group[hidden],
|
.tool-group[hidden],
|
||||||
.page-list[hidden],
|
.page-list[hidden],
|
||||||
.page-frame[hidden],
|
.page-frame[hidden],
|
||||||
@@ -1513,6 +1516,10 @@ button.outline-link:hover {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-object.edit-background:not(.selected):not(.erasing) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-object.edit-contour,
|
.edit-object.edit-contour,
|
||||||
.edit-object.edit-contour.selected,
|
.edit-object.edit-contour.selected,
|
||||||
.edit-object.edit-contour.erasing {
|
.edit-object.edit-contour.erasing {
|
||||||
|
|||||||
+61
-6
@@ -27,6 +27,7 @@ let textMeasure = null;
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
composite: null,
|
composite: null,
|
||||||
|
annotationEdit: null,
|
||||||
ready: false,
|
ready: false,
|
||||||
wasmExited: false,
|
wasmExited: false,
|
||||||
wasmSeq: 0,
|
wasmSeq: 0,
|
||||||
@@ -334,6 +335,13 @@ const el = {
|
|||||||
signatureSummary: document.querySelector("#signatureSummary"),
|
signatureSummary: document.querySelector("#signatureSummary"),
|
||||||
signatureList: document.querySelector("#signatureList"),
|
signatureList: document.querySelector("#signatureList"),
|
||||||
annotationNote: document.querySelector("#annotationNote"),
|
annotationNote: document.querySelector("#annotationNote"),
|
||||||
|
annotationForm: document.querySelector("#annotationForm"),
|
||||||
|
annotationFields: document.querySelector("#annotationFields"),
|
||||||
|
annotationRemark: document.querySelector("#annotationRemark"),
|
||||||
|
annotationCreator: document.querySelector("#annotationCreator"),
|
||||||
|
annotationStatus: document.querySelector("#annotationStatus"),
|
||||||
|
annotationClose: document.querySelector("#annotationClose"),
|
||||||
|
annotationSubmit: document.querySelector("#annotationSubmit"),
|
||||||
annotationDetail: document.querySelector("#annotationDetail"),
|
annotationDetail: document.querySelector("#annotationDetail"),
|
||||||
docFontList: document.querySelector("#docFontList"),
|
docFontList: document.querySelector("#docFontList"),
|
||||||
docFontSummary: document.querySelector("#docFontSummary"),
|
docFontSummary: document.querySelector("#docFontSummary"),
|
||||||
@@ -363,7 +371,9 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
setStatus(reason || pageStatus(state.pageIndex, state.doc.pageCount));
|
setStatus(reason || pageStatus(state.pageIndex, state.doc.pageCount));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTransform: (item, change) => changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item,
|
onTransform: (item, change) => (item.items || [item]).every(object => object.type === "Annotation")
|
||||||
|
? changeAnnotations(item, "move", change.x, change.y)
|
||||||
|
: 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),
|
||||||
onNudgeChange: () => {
|
onNudgeChange: () => {
|
||||||
updatePendingChanges();
|
updatePendingChanges();
|
||||||
@@ -379,8 +389,13 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
syncSelection();
|
syncSelection();
|
||||||
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||||
},
|
},
|
||||||
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
onDelete: (item) => (item.items || [item]).every(object => object.type === "Annotation")
|
||||||
|
? changeAnnotations(item, "delete") : changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
||||||
onErase: (items, box, points) => {
|
onErase: (items, box, points) => {
|
||||||
|
if (items.some(item => item.type === "Annotation")) {
|
||||||
|
if (box || !items.every(item => item.type === "Annotation")) { setStatus(box ? "注解暂不支持局部擦除" : "注解与正文需分别操作"); return false; }
|
||||||
|
return changeAnnotations({ items, index: items[0].index, id: items.map(item => item.id) }, "delete");
|
||||||
|
}
|
||||||
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
|
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
|
||||||
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
|
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
|
||||||
const item = { index: items[0].index, id: items.map(item => item.id), scoped: items.every(item => item.scoped) };
|
const item = { index: items[0].index, id: items.map(item => item.id), scoped: items.every(item => item.scoped) };
|
||||||
@@ -480,6 +495,16 @@ el.infoForm.addEventListener("submit", async event => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value)) el.infoPanel.close();
|
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value)) el.infoPanel.close();
|
||||||
});
|
});
|
||||||
|
el.annotationClose.addEventListener("click", () => el.annotationNote.close());
|
||||||
|
el.annotationNote.addEventListener("close", () => { state.annotationEdit = null; });
|
||||||
|
el.annotationForm.addEventListener("submit", async event => {
|
||||||
|
if (!state.annotationEdit) return;
|
||||||
|
event.preventDefault();
|
||||||
|
if (await changeAnnotations(state.annotationEdit, "update", el.annotationRemark.value, el.annotationCreator.value)) {
|
||||||
|
el.annotationNote.close();
|
||||||
|
canvasEditor.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
el.paragraphForm.addEventListener("submit", async event => {
|
el.paragraphForm.addEventListener("submit", async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (document.body.hasAttribute("aria-busy") || !el.paragraphForm.reportValidity()) return;
|
if (document.body.hasAttribute("aria-busy") || !el.paragraphForm.reportValidity()) return;
|
||||||
@@ -1370,6 +1395,18 @@ async function createDocument(event) {
|
|||||||
|
|
||||||
async function editCanvasObject(item) {
|
async function editCanvasObject(item) {
|
||||||
if (!state.editing || document.body.hasAttribute("aria-busy") || canvasEditor.input || item.items) return;
|
if (!state.editing || document.body.hasAttribute("aria-busy") || canvasEditor.input || item.items) return;
|
||||||
|
if (item.type === "Annotation") {
|
||||||
|
state.annotationEdit = item;
|
||||||
|
el.annotationRemark.value = item.remark || "";
|
||||||
|
el.annotationCreator.value = item.creator || "";
|
||||||
|
el.annotationFields.hidden = el.annotationSubmit.hidden = false;
|
||||||
|
el.annotationDetail.hidden = true;
|
||||||
|
el.annotationStatus.textContent = "";
|
||||||
|
el.annotationClose.textContent = "取消";
|
||||||
|
el.annotationNote.showModal();
|
||||||
|
el.annotationRemark.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
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;
|
||||||
@@ -1403,7 +1440,17 @@ async function editCanvasObject(item) {
|
|||||||
|
|
||||||
function mountEditorObjects(index, page, surface) {
|
function mountEditorObjects(index, page, surface) {
|
||||||
const scope = state.composite?.index === index ? state.composite : null;
|
const scope = state.composite?.index === index ? state.composite : null;
|
||||||
canvasEditor.mount(index, scope ? { ...page, objects: scope.objects } : page, surface);
|
const annotations = state.renderAnnotations ? (page.annotations || []).filter(annotation => annotation.visible).map(annotation => ({
|
||||||
|
...annotation, id: `annotation:${annotation.id}`, type: "Annotation", width: annotation.width || 6, height: annotation.height || 6,
|
||||||
|
background: annotation.type === "Watermark" || annotation.x <= 0.1 && annotation.y <= 0.1
|
||||||
|
&& annotation.x + annotation.width >= page.width - 0.1 && annotation.y + annotation.height >= page.height - 0.1,
|
||||||
|
capabilities: { update: true, move: true, delete: true },
|
||||||
|
})) : [];
|
||||||
|
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : [...page.objects, ...annotations] }, surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAnnotations(item, action, ...args) {
|
||||||
|
return changeDocument("ofdgoChangeAnnotations", { ...item, id: item.items ? item.id : [item.id] }, action, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCompositeScope() {
|
function resetCompositeScope() {
|
||||||
@@ -1953,7 +2000,8 @@ async function changeDocument(name, item, ...args) {
|
|||||||
rememberEditorView(before, revision);
|
rememberEditorView(before, revision);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const clearSelection = !item || ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
|
const clearSelection = !item || name === "ofdgoChangeAnnotations" && args[0] === "delete"
|
||||||
|
|| ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
|
||||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoGroupObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoGroupObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
canvasEditor.setTool("");
|
canvasEditor.setTool("");
|
||||||
@@ -1981,7 +2029,9 @@ async function changeDocument(name, item, ...args) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
if (el.batchPagesPanel.open) {
|
if (el.annotationNote.open) {
|
||||||
|
el.annotationStatus.textContent = err.message;
|
||||||
|
} else if (el.batchPagesPanel.open) {
|
||||||
el.batchPagesStatus.textContent = err.message;
|
el.batchPagesStatus.textContent = err.message;
|
||||||
} else if (el.objectStylePanel.open) {
|
} else if (el.objectStylePanel.open) {
|
||||||
el.objectStyleStatus.textContent = err.message;
|
el.objectStyleStatus.textContent = err.message;
|
||||||
@@ -5270,6 +5320,10 @@ function mountAnnotationNotes(index, surface, annotations) {
|
|||||||
note.addEventListener("click", () => {
|
note.addEventListener("click", () => {
|
||||||
if (state.editing || document.body.hasAttribute("aria-busy")) return;
|
if (state.editing || document.body.hasAttribute("aria-busy")) return;
|
||||||
el.annotationDetail.replaceChildren();
|
el.annotationDetail.replaceChildren();
|
||||||
|
el.annotationFields.hidden = el.annotationSubmit.hidden = true;
|
||||||
|
el.annotationDetail.hidden = false;
|
||||||
|
el.annotationStatus.textContent = "";
|
||||||
|
el.annotationClose.textContent = "关闭";
|
||||||
const content = document.createElement("div");
|
const content = document.createElement("div");
|
||||||
content.textContent = annotation.remark;
|
content.textContent = annotation.remark;
|
||||||
el.annotationDetail.append(content);
|
el.annotationDetail.append(content);
|
||||||
@@ -5746,7 +5800,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
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" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||||
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
|
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
|
||||||
el.editObjectButton.title = canEditObject(item, "enter") ? "进入组合" : item?.type === "ImageObject" ? "替换图片" : "修改对象";
|
el.editObjectButton.title = canEditObject(item, "enter") ? "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
||||||
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
||||||
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);
|
||||||
@@ -5866,6 +5920,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
el.pageForm.inert = busy;
|
el.pageForm.inert = busy;
|
||||||
el.paragraphForm.inert = busy;
|
el.paragraphForm.inert = busy;
|
||||||
el.infoForm.inert = busy;
|
el.infoForm.inert = busy;
|
||||||
|
el.annotationForm.inert = busy;
|
||||||
el.importForm.inert = busy && !state.importing;
|
el.importForm.inert = busy && !state.importing;
|
||||||
for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy;
|
for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy;
|
||||||
el.importPages.disabled = busy || el.importRange.value !== "custom";
|
el.importPages.disabled = busy || el.importRange.value !== "custom";
|
||||||
|
|||||||
@@ -176,7 +176,10 @@ function paintShape(node, shape, box) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function canEditObject(item, capability) {
|
export function canEditObject(item, capability) {
|
||||||
return Boolean(item) && (item.items || [item]).every(object => Boolean(object.capabilities?.[capability]));
|
if (!item) return false;
|
||||||
|
const objects = item.items || [item];
|
||||||
|
if (objects.some(object => object.type === "Annotation") && objects.some(object => object.type !== "Annotation")) return false;
|
||||||
|
return objects.every(object => Boolean(object.capabilities?.[capability] || capability === "move" && object.capabilities?.transform));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function missingGlyphMessage(diagnostic, action = "请更换字体") {
|
export function missingGlyphMessage(diagnostic, action = "请更换字体") {
|
||||||
@@ -188,6 +191,7 @@ export function missingGlyphMessage(diagnostic, action = "请更换字体") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function objectEditReason(item) {
|
export function objectEditReason(item) {
|
||||||
|
if (item?.items?.some(object => object.type === "Annotation") && item.items.some(object => object.type !== "Annotation")) return "注解与正文需分别操作";
|
||||||
const labels = { fontUnavailable: "字体不可用", unsupportedColor: "暂不支持此颜色", unsupportedStyle: "暂不支持此样式",
|
const labels = { fontUnavailable: "字体不可用", unsupportedColor: "暂不支持此颜色", unsupportedStyle: "暂不支持此样式",
|
||||||
unsupportedContainer: "暂不支持此对象结构", invalidObject: "对象数据异常" };
|
unsupportedContainer: "暂不支持此对象结构", invalidObject: "对象数据异常" };
|
||||||
const reasons = (item?.items || (item ? [item] : [])).map(object => {
|
const reasons = (item?.items || (item ? [item] : [])).map(object => {
|
||||||
@@ -278,18 +282,20 @@ export class CanvasEditor {
|
|||||||
const layer = document.createElement("div");
|
const layer = document.createElement("div");
|
||||||
layer.className = "edit-layer";
|
layer.className = "edit-layer";
|
||||||
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object], [data-ofd-child]")].map((node, order) => [node.getAttribute("data-ofd-object") || node.getAttribute("data-ofd-child"), { node, order }]));
|
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object], [data-ofd-child]")].map((node, order) => [node.getAttribute("data-ofd-object") || node.getAttribute("data-ofd-child"), { node, order }]));
|
||||||
const objects = [...page.objects].sort((a, b) => (artwork.get(a.id)?.order ?? 0) - (artwork.get(b.id)?.order ?? 0));
|
const objects = [...page.objects].sort((a, b) => Number(Boolean(b.background)) - Number(Boolean(a.background))
|
||||||
|
|| (artwork.get(a.id)?.order ?? 0) - (artwork.get(b.id)?.order ?? 0));
|
||||||
for (const object of objects) {
|
for (const object of 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.classList.toggle("edit-background", Boolean(object.background));
|
||||||
|
node.classList.toggle("read-only", !canEditObject(object, "move"));
|
||||||
node.title = objectEditReason(object);
|
node.title = objectEditReason(object);
|
||||||
if (lineShape(object.shape)) {
|
if (lineShape(object.shape)) {
|
||||||
node.classList.add("edit-line");
|
node.classList.add("edit-line");
|
||||||
}
|
}
|
||||||
node.tabIndex = 0;
|
node.tabIndex = 0;
|
||||||
node.setAttribute("role", "button");
|
node.setAttribute("role", "button");
|
||||||
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象", CompositeObject: "复合对象", CompositeGraphicUnit: "复合对象" }[object.type]);
|
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象", CompositeObject: "复合对象", CompositeGraphicUnit: "复合对象", Annotation: "注解对象" }[object.type]);
|
||||||
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
||||||
item.artwork = artwork.get(object.id)?.node;
|
item.artwork = artwork.get(object.id)?.node;
|
||||||
if (object.type === "PathObject" && (object.shape || object.outline) || (object.scoped || object.type === "CompositeObject" || object.type === "CompositeGraphicUnit") && object.contours?.length) {
|
if (object.type === "PathObject" && (object.shape || object.outline) || (object.scoped || object.type === "CompositeObject" || object.type === "CompositeGraphicUnit") && object.contours?.length) {
|
||||||
@@ -527,8 +533,9 @@ export class CanvasEditor {
|
|||||||
node = target?.node;
|
node = target?.node;
|
||||||
this.focus();
|
this.focus();
|
||||||
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
||||||
if (!target) {
|
if (!target || target.background && !this.items().includes(target)) {
|
||||||
this.startMarquee(event, additive);
|
this.startMarquee(event, additive);
|
||||||
|
if (this.drag?.marquee) this.drag.target = target;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (additive && !event.target.dataset.corner) {
|
if (additive && !event.target.dataset.corner) {
|
||||||
@@ -540,7 +547,7 @@ export class CanvasEditor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = this.selected;
|
const item = this.selected;
|
||||||
if (!canEditObject(item, "transform")) return;
|
if (!canEditObject(item, "move")) return;
|
||||||
this.drag = {
|
this.drag = {
|
||||||
item, pointerID: event.pointerId, corner: !item.items || target === item ? resizeCorner(node, event) : "",
|
item, pointerID: event.pointerId, corner: !item.items || target === item ? resizeCorner(node, event) : "",
|
||||||
rect: item.surface.getBoundingClientRect(), rotation: this.options.rotation(),
|
rect: item.surface.getBoundingClientRect(), rotation: this.options.rotation(),
|
||||||
@@ -739,6 +746,7 @@ export class CanvasEditor {
|
|||||||
this.drag = null;
|
this.drag = null;
|
||||||
drag.preview.remove();
|
drag.preview.remove();
|
||||||
this.viewer.releasePointerCapture(drag.pointerID);
|
this.viewer.releasePointerCapture(drag.pointerID);
|
||||||
|
if (drag.target && !drag.moved) this.setSelection([...drag.base, drag.target]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (drag.shape) {
|
if (drag.shape) {
|
||||||
@@ -810,7 +818,8 @@ export class CanvasEditor {
|
|||||||
if (!page) return;
|
if (!page) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.focus();
|
this.focus();
|
||||||
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)),
|
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node))
|
||||||
|
.filter(item => this.tool === "erase-object" || !item.background),
|
||||||
erased: new Set(), shape: "rectangle", pointerID: event.pointerId, clientX: event.clientX, clientY: event.clientY,
|
erased: new Set(), shape: "rectangle", pointerID: event.pointerId, clientX: event.clientX, clientY: event.clientY,
|
||||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
||||||
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
|
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
|
||||||
@@ -866,9 +875,11 @@ export class CanvasEditor {
|
|||||||
const box = { x: Math.min(from.x, to.x), y: Math.min(from.y, to.y), width: Math.abs(to.x-from.x), height: Math.abs(to.y-from.y) };
|
const box = { x: Math.min(from.x, to.x), y: Math.min(from.y, to.y), width: Math.abs(to.x-from.x), height: Math.abs(to.y-from.y) };
|
||||||
paintShape(drag.node, "rectangle", box);
|
paintShape(drag.node, "rectangle", box);
|
||||||
if (Math.hypot(event.clientX-drag.clientX, event.clientY-drag.clientY) < 3) return;
|
if (Math.hypot(event.clientX-drag.clientX, event.clientY-drag.clientY) < 3) return;
|
||||||
|
drag.moved = true;
|
||||||
const items = [...drag.base];
|
const items = [...drag.base];
|
||||||
for (const node of drag.surface.querySelectorAll(".edit-object")) {
|
for (const node of drag.surface.querySelectorAll(".edit-object")) {
|
||||||
const item = this.nodes.get(node);
|
const item = this.nodes.get(node);
|
||||||
|
if (item.background && (item.x < box.x || item.y < box.y || item.x + item.width > box.x + box.width || item.y + item.height > box.y + box.height)) continue;
|
||||||
if (!items.includes(item) && item.x <= box.x+box.width && item.x+item.width >= box.x && item.y <= box.y+box.height && item.y+item.height >= box.y) {
|
if (!items.includes(item) && item.x <= box.x+box.width && item.x+item.width >= box.x && item.y <= box.y+box.height && item.y+item.height >= box.y) {
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
@@ -1030,7 +1041,7 @@ export class CanvasEditor {
|
|||||||
} else if (edit && !this.drag && !this.selected.items) {
|
} else if (edit && !this.drag && !this.selected.items) {
|
||||||
if (this.nudge) this.commitNudge().then(saved => { if (saved && this.selected) this.options.onEdit(this.selected); });
|
if (this.nudge) this.commitNudge().then(saved => { if (saved && this.selected) this.options.onEdit(this.selected); });
|
||||||
else this.options.onEdit(this.selected);
|
else this.options.onEdit(this.selected);
|
||||||
} else if (!this.drag && directions[event.key] && canEditObject(this.selected, "transform")) {
|
} else if (!this.drag && directions[event.key] && canEditObject(this.selected, "move")) {
|
||||||
const [x, y] = directions[event.key];
|
const [x, y] = directions[event.key];
|
||||||
const [dx, dy] = [[x, y], [y, -x], [-x, -y], [-y, x]][this.options.rotation() / 90];
|
const [dx, dy] = [[x, y], [y, -x], [-x, -y], [-y, x]][this.options.rotation() / 90];
|
||||||
const step = event.shiftKey ? 10 : 1;
|
const step = event.shiftKey ? 10 : 1;
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoChangePage", changePage)
|
registerCallback("ofdgoChangePage", changePage)
|
||||||
registerCallback("ofdgoBatchPages", batchPages)
|
registerCallback("ofdgoBatchPages", batchPages)
|
||||||
registerCallback("ofdgoChangeOutline", changeOutline)
|
registerCallback("ofdgoChangeOutline", changeOutline)
|
||||||
|
registerCallback("ofdgoChangeAnnotations", changeAnnotations)
|
||||||
registerCallback("ofdgoStyleObjects", styleObjects)
|
registerCallback("ofdgoStyleObjects", styleObjects)
|
||||||
registerCallback("ofdgoMoveOutline", moveOutline)
|
registerCallback("ofdgoMoveOutline", moveOutline)
|
||||||
registerCallback("ofdgoCaptureStyle", captureStyle)
|
registerCallback("ofdgoCaptureStyle", captureStyle)
|
||||||
@@ -854,6 +855,31 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// changeAnnotations 修改注解说明、平移或删除同页注解,复用会话历史与预览
|
||||||
|
// 入参: args 页面索引、注解标识数组、操作及参数
|
||||||
|
// 返回: any 编辑状态, error 错误信息
|
||||||
|
func changeAnnotations(args []js.Value) (any, error) {
|
||||||
|
return changeObjects(func() error {
|
||||||
|
ids := stringsFromJS(args[1])
|
||||||
|
for i := range ids {
|
||||||
|
ids[i] = strings.TrimPrefix(ids[i], "annotation:")
|
||||||
|
}
|
||||||
|
switch args[2].String() {
|
||||||
|
case "update":
|
||||||
|
if len(ids) != 1 {
|
||||||
|
return fmt.Errorf("select one annotation to update")
|
||||||
|
}
|
||||||
|
return currentEditor.UpdateAnnotation(args[0].Int(), ids[0], args[3].String(), args[4].String())
|
||||||
|
case "move":
|
||||||
|
return currentEditor.MoveAnnotations(args[0].Int(), ids, args[3].Float(), args[4].Float())
|
||||||
|
case "delete":
|
||||||
|
return currentEditor.DeleteAnnotations(args[0].Int(), ids)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported annotation action %q", args[2].String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// editorCapabilities 统一顶层与内部对象的操作能力和受限原因
|
// editorCapabilities 统一顶层与内部对象的操作能力和受限原因
|
||||||
// 入参: capability 库层能力, kind 对象类型
|
// 入参: capability 库层能力, kind 对象类型
|
||||||
// 返回: map[string]any 前端能力
|
// 返回: map[string]any 前端能力
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
// 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"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Annotations 获取指定页面的注解信息,包含不可见注解,返回值与编辑器相互独立
|
||||||
|
// 入参: index 页面索引
|
||||||
|
// 返回: []AnnotationInfo 注解信息, error 错误信息
|
||||||
|
func (e *Editor) Annotations(index int) ([]AnnotationInfo, error) {
|
||||||
|
if index < 0 || index >= len(e.pages) {
|
||||||
|
return nil, fmt.Errorf("page index %d out of range", index)
|
||||||
|
}
|
||||||
|
if e.source == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return e.source.reader.annotationInfos(index, e.pages[index].ID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAnnotation 修改注解说明和作者,保留外观、动作、参数及扩展字段
|
||||||
|
// 有效修改更新LastModDate并计入一次撤销记录,不受原文件ReadOnly声明限制
|
||||||
|
// 入参: index 页面索引, id 注解标识, remark 说明, creator 作者
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) UpdateAnnotation(index int, id, remark, creator string) error {
|
||||||
|
return e.editAnnotations(index, []string{id}, func(data []byte, node *editorXML) ([]byte, error) {
|
||||||
|
standalone, err := editorXMLStandalone(data[node.start:node.end], node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var annotation Annotation
|
||||||
|
if err := xml.Unmarshal(standalone, &annotation); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if annotation.Remark == remark && annotation.Creator == creator {
|
||||||
|
return data[node.start:node.end], nil
|
||||||
|
}
|
||||||
|
data = standalone
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if annotation.Remark != remark {
|
||||||
|
var escaped bytes.Buffer
|
||||||
|
if err := xml.EscapeText(&escaped, []byte(remark)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if child := root.child("Remark"); child != nil {
|
||||||
|
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, child, escaped.Bytes())})
|
||||||
|
} else if root.open == root.end {
|
||||||
|
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, root, editorXMLText("Remark", remark))})
|
||||||
|
} else {
|
||||||
|
data = editorPatchXML(data, []editorXMLPatch{{root.open, root.open, editorXMLText("Remark", remark)}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root, err = parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if annotation.Creator != creator {
|
||||||
|
data, err = editorXMLAttribute(data, root, "Creator", creator)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return editorAnnotationDate(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveAnnotations 平移同页注解,保留内部图元、动作坐标及原始尺寸,一次操作计入一条撤销记录
|
||||||
|
// 入参: index 页面索引, ids 不重复的注解标识, dx 横向毫米偏移, dy 纵向毫米偏移
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) MoveAnnotations(index int, ids []string, dx, dy float64) error {
|
||||||
|
if !finite(dx) || !finite(dy) {
|
||||||
|
return fmt.Errorf("annotation offset must be finite")
|
||||||
|
}
|
||||||
|
return e.editAnnotations(index, ids, func(data []byte, node *editorXML) ([]byte, error) {
|
||||||
|
appearance := node.child("Appearance")
|
||||||
|
if appearance == nil {
|
||||||
|
return nil, fmt.Errorf("annotation %q has no appearance", node.attr("ID"))
|
||||||
|
}
|
||||||
|
box, err := ParseBox(appearance.attr("Boundary"))
|
||||||
|
if err != nil || !finite(box.X+dx) || !finite(box.Y+dy) || !finite(box.W) || !finite(box.H) || box.W < 0 || box.H < 0 {
|
||||||
|
return nil, fmt.Errorf("annotation %q has invalid boundary", node.attr("ID"))
|
||||||
|
}
|
||||||
|
if dx == 0 && dy == 0 {
|
||||||
|
return data[node.start:node.end], nil
|
||||||
|
}
|
||||||
|
data, err = editorXMLStandalone(data[node.start:node.end], node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
appearance = root.child("Appearance")
|
||||||
|
boundary := fmt.Sprintf("%s %s %s %s", ofdNumber(box.X+dx), ofdNumber(box.Y+dy), ofdNumber(box.W), ofdNumber(box.H))
|
||||||
|
updated, err := editorXMLAttribute(data, appearance, "Boundary", boundary)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return editorAnnotationDate(editorPatchXML(data, []editorXMLPatch{{appearance.start, appearance.end, updated}}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAnnotations 删除同页注解并清理空的页面注解引用,一次操作计入一条撤销记录
|
||||||
|
// 入参: index 页面索引, ids 不重复的注解标识
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) DeleteAnnotations(index int, ids []string) error {
|
||||||
|
return e.editAnnotations(index, ids, func([]byte, *editorXML) ([]byte, error) { return nil, nil })
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorAnnotationDate 更新注解修改日期,采用标准xs:date格式
|
||||||
|
// 入参: data 注解XML片段
|
||||||
|
// 返回: []byte 新片段, error 错误信息
|
||||||
|
func editorAnnotationDate(data []byte) ([]byte, error) {
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return editorXMLAttribute(data, root, "LastModDate", time.Now().Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// editAnnotations 原子修改页面注解原文,按需分离共用文件并保留输入包与历史快照
|
||||||
|
// 入参: index 页面索引, ids 注解标识, edit 单个注解修改函数
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *editorXML) ([]byte, error)) error {
|
||||||
|
if index < 0 || index >= len(e.pages) {
|
||||||
|
return fmt.Errorf("page index %d out of range", index)
|
||||||
|
}
|
||||||
|
wanted := make(map[string]bool, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
if id == "" || wanted[id] {
|
||||||
|
return fmt.Errorf("missing or duplicate annotation ID %q", id)
|
||||||
|
}
|
||||||
|
wanted[id] = true
|
||||||
|
}
|
||||||
|
if len(wanted) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if e.source == nil || e.source.document.Annotations == "" {
|
||||||
|
return fmt.Errorf("page has no annotations")
|
||||||
|
}
|
||||||
|
reader := e.source.reader
|
||||||
|
name, err := editorPageLocation(reader, nil, "", "/"+reader.ResPath(e.source.document.Annotations))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := reader.readFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parts := make(map[string][]byte)
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
found := make(map[string]bool, len(ids))
|
||||||
|
remaining := 0
|
||||||
|
for _, ref := range root.children {
|
||||||
|
if ref.name.Local != "Page" || ref.name.Space != root.name.Space {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remaining++
|
||||||
|
if ref.attr("PageID") != e.pages[index].ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
loc := ref.child("FileLoc")
|
||||||
|
if loc == nil {
|
||||||
|
return fmt.Errorf("annotation page has no FileLoc")
|
||||||
|
}
|
||||||
|
file, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, loc)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content, err := reader.readFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
page, err := parseEditorXML(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var changes []editorXMLPatch
|
||||||
|
count := 0
|
||||||
|
for _, node := range page.children {
|
||||||
|
if node.name.Local != "Annot" || node.name.Space != page.name.Space {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
id := node.attr("ID")
|
||||||
|
if !wanted[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if found[id] {
|
||||||
|
return fmt.Errorf("ambiguous annotation ID %q", id)
|
||||||
|
}
|
||||||
|
found[id] = true
|
||||||
|
value, err := edit(content, node)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if value == nil {
|
||||||
|
count--
|
||||||
|
}
|
||||||
|
if !bytes.Equal(value, content[node.start:node.end]) {
|
||||||
|
changes = append(changes, editorXMLPatch{node.start, node.end, value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(changes) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sharedFile := false
|
||||||
|
for _, other := range root.children {
|
||||||
|
if other == ref || other.name.Local != "Page" || other.name.Space != root.name.Space || other.child("FileLoc") == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
shared, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, other.child("FileLoc"))))
|
||||||
|
if err == nil && shared == file {
|
||||||
|
sharedFile = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
patches = append(patches, editorXMLPatch{ref.start, ref.end, nil})
|
||||||
|
remaining--
|
||||||
|
if !sharedFile {
|
||||||
|
parts[file] = editorPatchXML(content, changes)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sharedFile {
|
||||||
|
file = path.Join(e.source.directory, "Annotations", e.pages[index].ID, file)
|
||||||
|
var escaped bytes.Buffer
|
||||||
|
_ = xml.EscapeText(&escaped, []byte("/"+file))
|
||||||
|
patches = append(patches, editorXMLContent(data, loc, escaped.Bytes()))
|
||||||
|
}
|
||||||
|
parts[file] = editorPatchXML(content, changes)
|
||||||
|
}
|
||||||
|
for _, id := range ids {
|
||||||
|
if !found[id] {
|
||||||
|
return fmt.Errorf("annotation %q not found", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(patches) != 0 {
|
||||||
|
parts[name] = editorPatchXML(data, patches)
|
||||||
|
if remaining == 0 {
|
||||||
|
docName, err := editorPageLocation(reader, nil, "", "/"+cleanPackagePath(reader.OFD.DocBody[0].DocRoot))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
docData, err := reader.readFile(docName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
doc, err := parseEditorXML(docData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parts[docName] = editorXMLSetText(docData, doc, [][2]string{{"Annotations", ""}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
files := maps.Clone(reader.files)
|
||||||
|
if files == nil {
|
||||||
|
files = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
maps.Copy(files, parts)
|
||||||
|
updated := &Reader{Zip: reader.Zip, files: files}
|
||||||
|
if err := updated.initRoot(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
doc, err := updated.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
before, after := e.source, *e.source
|
||||||
|
after.reader, after.document = updated, doc
|
||||||
|
e.source = &after
|
||||||
|
if change := e.recordChange(); change != nil {
|
||||||
|
change.undo = func(e *Editor) { e.source = before }
|
||||||
|
change.redo = func(e *Editor) { e.source = &after }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+2
-17
@@ -158,7 +158,7 @@ func (e *Editor) UpdateOutline(path []int, title string, page int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
value, err := editorOutlineAttribute(updated, node, "Title", title)
|
value, err := editorXMLAttribute(updated, node, "Title", title)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -326,21 +326,6 @@ func editorOutlineNode(root *editorXML, path []int) (*editorXML, error) {
|
|||||||
return root, nil
|
return root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorOutlineAttribute 修改单个标准属性,保留节点其余原文
|
|
||||||
// 入参: data 原文, node 节点, name 属性名, value 新值
|
|
||||||
// 返回: []byte 修改后的节点, error 错误信息
|
|
||||||
func editorOutlineAttribute(data []byte, node *editorXML, name, value string) ([]byte, error) {
|
|
||||||
before, err := editorXMLContainer(node.name.Local, ofdAttrs{{Name: xml.Name{Local: name}, Value: node.attr(name)}}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
after, err := editorXMLContainer(node.name.Local, ofdAttrs{{Name: xml.Name{Local: name}, Value: value}}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return editorXMLMerge(data, node, before, after)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setOutlineXML 更新目录及可选叶节点计数,保存一次撤销记录
|
// setOutlineXML 更新目录及可选叶节点计数,保存一次撤销记录
|
||||||
// 入参: data 新目录原文
|
// 入参: data 新目录原文
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -369,7 +354,7 @@ func (e *Editor) setOutlineXML(data []byte) error {
|
|||||||
for _, child := range editorOutlineChildren(node) {
|
for _, child := range editorOutlineChildren(node) {
|
||||||
count += leaves(child)
|
count += leaves(child)
|
||||||
}
|
}
|
||||||
value, err := editorOutlineAttribute(data, node, "Count", strconv.Itoa(count))
|
value, err := editorXMLAttribute(data, node, "Count", strconv.Itoa(count))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type editorResourceRefs struct {
|
|||||||
// 入参: parts 已修改和新增的包内条目, progress 保存进度回调
|
// 入参: parts 已修改和新增的包内条目, progress 保存进度回调
|
||||||
// 返回: map[string]bool 可移除的二进制条目, error 读取错误
|
// 返回: map[string]bool 可移除的二进制条目, error 读取错误
|
||||||
func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) {
|
func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) {
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 && e.revision == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
|
|||||||
@@ -546,6 +546,21 @@ func editorXMLEncodedFragment(data []byte, node *editorXML) []byte {
|
|||||||
return editorPatchXML(fragment, []editorXMLPatch{{position, position, []byte(" xmlns:ofd=\"" + ofdNamespace + "\"")}})
|
return editorPatchXML(fragment, []editorXMLPatch{{position, position, []byte(" xmlns:ofd=\"" + ofdNamespace + "\"")}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorXMLAttribute 修改单个标准属性,保留节点其余原文
|
||||||
|
// 入参: data 原文, node 节点, name 属性名, value 新值
|
||||||
|
// 返回: []byte 修改后的节点, error 错误信息
|
||||||
|
func editorXMLAttribute(data []byte, node *editorXML, name, value string) ([]byte, error) {
|
||||||
|
before, err := editorXMLContainer(node.name.Local, ofdAttrs{{Name: xml.Name{Local: name}, Value: node.attr(name)}}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
after, err := editorXMLContainer(node.name.Local, ofdAttrs{{Name: xml.Name{Local: name}, Value: value}}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return editorXMLMerge(data, node, before, after)
|
||||||
|
}
|
||||||
|
|
||||||
// editorXMLContainer 用标准编码器封装已编码的子节点
|
// editorXMLContainer 用标准编码器封装已编码的子节点
|
||||||
// 入参: name 节点名称, attrs 属性, content 子节点XML
|
// 入参: name 节点名称, attrs 属性, content 子节点XML
|
||||||
// 返回: []byte XML片段, error 错误信息
|
// 返回: []byte XML片段, error 错误信息
|
||||||
|
|||||||
@@ -23,11 +23,19 @@ func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH f
|
|||||||
if annot.Visible != nil && !*annot.Visible {
|
if annot.Visible != nil && !*annot.Visible {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
groups, grouped := ctx.Renderer.(interface {
|
||||||
|
beginAnnotation(string) bool
|
||||||
|
endObject()
|
||||||
|
})
|
||||||
|
grouped = grouped && groups.beginAnnotation(annot.ID)
|
||||||
box, _ := ParseBox(annot.Appearance.Boundary)
|
box, _ := ParseBox(annot.Appearance.Boundary)
|
||||||
ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y}
|
ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y}
|
||||||
for i := range annot.Appearance.Objects {
|
for i := range annot.Appearance.Objects {
|
||||||
r.renderObject(ctx, &annot.Appearance.Objects[i], pageH, nil, &ctm, false, nil)
|
r.renderObject(ctx, &annot.Appearance.Objects[i], pageH, nil, &ctm, false, nil)
|
||||||
}
|
}
|
||||||
|
if grouped {
|
||||||
|
groups.endObject()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-2
@@ -88,7 +88,7 @@ func (r *Renderer) RenderToSVGWithResources(page *PageContent, writer io.Writer)
|
|||||||
return r.renderSVGResources(page, writer, true, false)
|
return r.renderSVGResources(page, writer, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderToSVGWithObjects 渲染SVG并为页面直接对象添加data-ofd-object分组,保持原绘制顺序和分离资源
|
// RenderToSVGWithObjects 渲染SVG并为页面对象和注解添加data-ofd-object分组,注解标识使用annotation:前缀
|
||||||
// 入参: page 页面内容, writer 输出流
|
// 入参: page 页面内容, writer 输出流
|
||||||
// 返回: SVGResources 字体和图片资源, error 错误信息
|
// 返回: SVGResources 字体和图片资源, error 错误信息
|
||||||
func (r *Renderer) RenderToSVGWithObjects(page *PageContent, writer io.Writer) (SVGResources, error) {
|
func (r *Renderer) RenderToSVGWithObjects(page *PageContent, writer io.Writer) (SVGResources, error) {
|
||||||
@@ -143,7 +143,7 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
|
|||||||
return SVGResources{Fonts: s.fonts, Images: s.images}, buffer.Flush()
|
return SVGResources{Fonts: s.fonts, Images: s.images}, buffer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// beginObject 标记直接对象及复合成员,模板与注释不单独标记
|
// beginObject 标记直接对象及复合成员,模板与注解内部图元不单独标记
|
||||||
// 入参: object 图形对象
|
// 入参: object 图形对象
|
||||||
// 返回: bool 是否写入分组
|
// 返回: bool 是否写入分组
|
||||||
func (s *svgResourceRenderer) beginObject(object *GraphicObject) bool {
|
func (s *svgResourceRenderer) beginObject(object *GraphicObject) bool {
|
||||||
@@ -169,6 +169,18 @@ func (s *svgResourceRenderer) endObject() {
|
|||||||
fmt.Fprint(s.writer, `</g>`)
|
fmt.Fprint(s.writer, `</g>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// beginAnnotation 标记完整注解外观,内部图元保持原有层级和绘制顺序
|
||||||
|
// 入参: id 注解标识
|
||||||
|
// 返回: bool 是否写入分组
|
||||||
|
func (s *svgResourceRenderer) beginAnnotation(id string) bool {
|
||||||
|
if s.objects == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
s.objectStack = append(s.objectStack, svgObjectGroup{})
|
||||||
|
fmt.Fprintf(s.writer, `<g data-ofd-object="annotation:%s">`, html.EscapeString(id))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// skipObjects 为不可见资源保留内部成员序号
|
// skipObjects 为不可见资源保留内部成员序号
|
||||||
// 入参: count 直接成员数量
|
// 入参: count 直接成员数量
|
||||||
func (s *svgResourceRenderer) skipObjects(count int) {
|
func (s *svgResourceRenderer) skipObjects(count int) {
|
||||||
|
|||||||
Reference in new issue
Block a user