mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +08:00
This commit is contained in:
1 parent
06b5df9173
commit
697a059162
18 files changed
+694
-130
No files matched your search
@@ -183,6 +183,12 @@
|
|||||||
<form id="objectStyleForm">
|
<form id="objectStyleForm">
|
||||||
<div class="form-row"><span>组合</span><div class="form-format"><button id="groupObjectsButton" class="button" type="button">组合</button><button id="ungroupObjectButton" class="button" type="button">解组</button></div></div>
|
<div class="form-row"><span>组合</span><div class="form-format"><button id="groupObjectsButton" class="button" type="button">组合</button><button id="ungroupObjectButton" class="button" type="button">解组</button></div></div>
|
||||||
<div class="form-row"><span>格式</span><div class="form-format"><button id="copyStyleButton" class="button" type="button" aria-label="复制格式">复制</button><button id="pasteStyleButton" class="button" type="button" aria-label="粘贴格式">粘贴</button></div></div>
|
<div class="form-row"><span>格式</span><div class="form-format"><button id="copyStyleButton" class="button" type="button" aria-label="复制格式">复制</button><button id="pasteStyleButton" class="button" type="button" aria-label="粘贴格式">粘贴</button></div></div>
|
||||||
|
<details id="objectLinkFields" hidden>
|
||||||
|
<summary>链接</summary>
|
||||||
|
<div class="form-row"><label for="objectLinkKind">目标</label><select id="objectLinkKind"><option value="none">不设</option><option value="uri">网址</option><option value="page">页面</option><option value="keep">原有</option></select></div>
|
||||||
|
<div class="form-row" id="objectLinkAddressRow" hidden><label for="objectLinkAddress">地址</label><input id="objectLinkAddress" type="text"></div>
|
||||||
|
<div class="form-row" id="objectLinkPageRow" hidden><label for="objectLinkPage">页码</label><input id="objectLinkPage" type="number" min="1" step="1"></div>
|
||||||
|
</details>
|
||||||
<div class="form-row"><label for="objectOpacity">透明</label><div class="form-unit"><input id="objectOpacity" type="number" min="0" max="100" step="any" aria-label="透明度" autofocus><span>%</span></div></div>
|
<div class="form-row"><label for="objectOpacity">透明</label><div class="form-unit"><input id="objectOpacity" type="number" min="0" max="100" step="any" aria-label="透明度" autofocus><span>%</span></div></div>
|
||||||
<details id="objectBorderFields" hidden>
|
<details id="objectBorderFields" hidden>
|
||||||
<summary>边框</summary>
|
<summary>边框</summary>
|
||||||
@@ -342,6 +348,14 @@
|
|||||||
<dialog id="objectPicker" class="form-dialog" aria-label="选取对象">
|
<dialog id="objectPicker" class="form-dialog" aria-label="选取对象">
|
||||||
<form method="dialog"><div id="objectPickerList" class="object-picker-list"></div><div class="form-actions"><button class="button" type="submit">关闭</button></div></form>
|
<form method="dialog"><div id="objectPickerList" class="object-picker-list"></div><div class="form-actions"><button class="button" type="submit">关闭</button></div></form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<dialog id="sourceTextPanel" class="form-dialog" aria-label="保留排版">
|
||||||
|
<form id="sourceTextForm">
|
||||||
|
<h2 class="form-title">保留排版</h2>
|
||||||
|
<div class="form-row"><label for="sourceTextValue">内容</label><textarea id="sourceTextValue" rows="3" dir="auto" aria-label="文字内容" required></textarea></div>
|
||||||
|
<div id="sourceTextPreview" aria-label="文字预览"></div>
|
||||||
|
<div class="form-actions"><span id="sourceTextStatus" class="form-status" role="status"></span><button id="sourceTextCancel" class="button" type="button">取消</button><button class="button primary" type="submit">确定</button></div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
||||||
<form id="annotationForm" method="dialog">
|
<form id="annotationForm" method="dialog">
|
||||||
<div id="annotationDetail" class="annotation-detail"></div>
|
<div id="annotationDetail" class="annotation-detail"></div>
|
||||||
|
|||||||
@@ -692,6 +692,27 @@ button:not(:disabled):hover {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sourceTextPanel {
|
||||||
|
width: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sourceTextValue {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sourceTextPreview {
|
||||||
|
height: 160px;
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sourceTextPreview svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.security-dialog {
|
.security-dialog {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-3
@@ -284,10 +284,22 @@ const el = {
|
|||||||
objectHeight: document.querySelector("#objectHeight"),
|
objectHeight: document.querySelector("#objectHeight"),
|
||||||
objectAspect: document.querySelector("#objectAspect"),
|
objectAspect: document.querySelector("#objectAspect"),
|
||||||
objectStylePanel: document.querySelector("#objectStylePanel"),
|
objectStylePanel: document.querySelector("#objectStylePanel"),
|
||||||
|
sourceTextPanel: document.querySelector("#sourceTextPanel"),
|
||||||
|
sourceTextForm: document.querySelector("#sourceTextForm"),
|
||||||
|
sourceTextValue: document.querySelector("#sourceTextValue"),
|
||||||
|
sourceTextPreview: document.querySelector("#sourceTextPreview"),
|
||||||
|
sourceTextStatus: document.querySelector("#sourceTextStatus"),
|
||||||
|
sourceTextCancel: document.querySelector("#sourceTextCancel"),
|
||||||
objectStyleForm: document.querySelector("#objectStyleForm"),
|
objectStyleForm: document.querySelector("#objectStyleForm"),
|
||||||
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
||||||
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
||||||
objectOpacity: document.querySelector("#objectOpacity"),
|
objectOpacity: document.querySelector("#objectOpacity"),
|
||||||
|
objectLinkFields: document.querySelector("#objectLinkFields"),
|
||||||
|
objectLinkKind: document.querySelector("#objectLinkKind"),
|
||||||
|
objectLinkAddress: document.querySelector("#objectLinkAddress"),
|
||||||
|
objectLinkPage: document.querySelector("#objectLinkPage"),
|
||||||
|
objectLinkAddressRow: document.querySelector("#objectLinkAddressRow"),
|
||||||
|
objectLinkPageRow: document.querySelector("#objectLinkPageRow"),
|
||||||
objectGradientFields: document.querySelector("#objectGradientFields"),
|
objectGradientFields: document.querySelector("#objectGradientFields"),
|
||||||
gradientTarget: document.querySelector("#gradientTarget"),
|
gradientTarget: document.querySelector("#gradientTarget"),
|
||||||
objectBorderFields: document.querySelector("#objectBorderFields"),
|
objectBorderFields: document.querySelector("#objectBorderFields"),
|
||||||
@@ -997,6 +1009,23 @@ el.objectBoundsForm.addEventListener("submit", async event => {
|
|||||||
if (saved) el.objectBoundsPanel.close();
|
if (saved) el.objectBoundsPanel.close();
|
||||||
});
|
});
|
||||||
el.objectStyleCancel.addEventListener("click", () => el.objectStylePanel.close());
|
el.objectStyleCancel.addEventListener("click", () => el.objectStylePanel.close());
|
||||||
|
el.sourceTextCancel.addEventListener("click", () => el.sourceTextPanel.close());
|
||||||
|
el.sourceTextValue.addEventListener("input", () => {
|
||||||
|
clearTimeout(state.sourceTextTimer);
|
||||||
|
state.sourceTextTimer = setTimeout(previewPositionedText, 150);
|
||||||
|
});
|
||||||
|
el.sourceTextPanel.addEventListener("close", () => {
|
||||||
|
clearTimeout(state.sourceTextTimer);
|
||||||
|
state.sourceText = null;
|
||||||
|
el.sourceTextPreview.replaceChildren();
|
||||||
|
});
|
||||||
|
el.sourceTextForm.addEventListener("submit", async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const item = state.sourceText?.item;
|
||||||
|
if (!item) return;
|
||||||
|
if (await changeDocument("ofdgoUpdateText", item, el.sourceTextValue.value, null, item.size, null)) el.sourceTextPanel.close();
|
||||||
|
});
|
||||||
|
el.objectLinkKind.addEventListener("change", updateObjectLinkFields);
|
||||||
el.gradientKind.addEventListener("change", () => {
|
el.gradientKind.addEventListener("change", () => {
|
||||||
el.gradientControls.hidden = el.gradientKind.value === "keep";
|
el.gradientControls.hidden = el.gradientKind.value === "keep";
|
||||||
el.gradientAngle.disabled = el.gradientKind.value !== "linear";
|
el.gradientAngle.disabled = el.gradientKind.value !== "linear";
|
||||||
@@ -1410,7 +1439,7 @@ function handleKeyDown(event) {
|
|||||||
|
|
||||||
function formDialogOpen() {
|
function formDialogOpen() {
|
||||||
return el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
return el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
||||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setObjectDimension(input, value) {
|
function setObjectDimension(input, value) {
|
||||||
@@ -1458,6 +1487,16 @@ function openObjectStyle() {
|
|||||||
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
||||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
||||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||||
|
el.objectLinkFields.hidden = items.length !== 1 || items[0].scoped;
|
||||||
|
const link = items[0].link ? JSON.parse(items[0].link) : null;
|
||||||
|
const targetPage = link?.Goto?.Dest ? state.doc.pages.findIndex(page => page.id === link.Goto.Dest.PageID) : -1;
|
||||||
|
el.objectLinkKind.value = items[0].linkCount > 1 ? "keep" : link?.URI ? "uri" : targetPage >= 0 ? "page" : link ? "keep" : "none";
|
||||||
|
el.objectLinkKind.disabled = items[0].linkCount > 1;
|
||||||
|
el.objectLinkAddress.value = link?.URI?.URI || "";
|
||||||
|
el.objectLinkPage.value = String(targetPage >= 0 ? targetPage + 1 : 1);
|
||||||
|
el.objectLinkPage.max = String(state.doc?.pages.length || 1);
|
||||||
|
state.objectLinkOriginal = { kind: el.objectLinkKind.value, address: el.objectLinkAddress.value, page: el.objectLinkPage.value, link };
|
||||||
|
updateObjectLinkFields();
|
||||||
el.objectBorderFields.hidden = items.length !== 1 || items[0].scoped || items[0].type !== "ImageObject";
|
el.objectBorderFields.hidden = items.length !== 1 || items[0].scoped || items[0].type !== "ImageObject";
|
||||||
const border = items[0].borderStyle ? JSON.parse(items[0].borderStyle) : null;
|
const border = items[0].borderStyle ? JSON.parse(items[0].borderStyle) : null;
|
||||||
el.borderEnabled.checked = Boolean(border);
|
el.borderEnabled.checked = Boolean(border);
|
||||||
@@ -1517,6 +1556,14 @@ function openObjectStyle() {
|
|||||||
|
|
||||||
function readObjectStyle() {
|
function readObjectStyle() {
|
||||||
const style = {}, changed = key => el[key].value !== state.styleOriginal[key];
|
const style = {}, changed = key => el[key].value !== state.styleOriginal[key];
|
||||||
|
if (!el.objectLinkFields.hidden) {
|
||||||
|
const original = state.objectLinkOriginal, kind = el.objectLinkKind.value;
|
||||||
|
if (kind !== "keep" && (kind !== original.kind || kind === "uri" && el.objectLinkAddress.value !== original.address || kind === "page" && el.objectLinkPage.value !== original.page)) {
|
||||||
|
style.link = kind === "none" ? null : JSON.stringify(kind === "uri"
|
||||||
|
? { URI: { ...original.link?.URI, URI: el.objectLinkAddress.value.trim() } }
|
||||||
|
: { Dest: { ...(original.link?.Goto?.Dest || { Type: "Fit" }), PageID: state.doc.pages[Number(el.objectLinkPage.value) - 1].id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!el.objectBorderFields.hidden) {
|
if (!el.objectBorderFields.hidden) {
|
||||||
const border = {};
|
const border = {};
|
||||||
if (el.borderEnabled.checked !== state.borderOriginal.Enabled) border.Enabled = el.borderEnabled.checked;
|
if (el.borderEnabled.checked !== state.borderOriginal.Enabled) border.Enabled = el.borderEnabled.checked;
|
||||||
@@ -1587,6 +1634,16 @@ function readObjectStyle() {
|
|||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateObjectLinkFields() {
|
||||||
|
const kind = el.objectLinkKind.value, visible = !el.objectLinkFields.hidden;
|
||||||
|
el.objectLinkAddressRow.hidden = !visible || kind !== "uri";
|
||||||
|
el.objectLinkPageRow.hidden = !visible || kind !== "page";
|
||||||
|
el.objectLinkAddress.disabled = el.objectLinkAddressRow.hidden;
|
||||||
|
el.objectLinkPage.disabled = el.objectLinkPageRow.hidden;
|
||||||
|
el.objectLinkAddress.required = visible && kind === "uri";
|
||||||
|
el.objectLinkPage.required = visible && kind === "page";
|
||||||
|
}
|
||||||
|
|
||||||
function gradientForm() {
|
function gradientForm() {
|
||||||
return {
|
return {
|
||||||
kind: el.gradientKind.value, angle: el.gradientAngle.value,
|
kind: el.gradientKind.value, angle: el.gradientAngle.value,
|
||||||
@@ -2084,7 +2141,18 @@ async function editCanvasObject(item) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item.type === "TextObject") {
|
if (item.type === "TextObject") {
|
||||||
if (!canEditObject(item, "textContent")) return;
|
if (!canEditObject(item, "textContent")) {
|
||||||
|
if (!item.scoped && canEditObject(item, "rewriteText")) {
|
||||||
|
state.sourceText = { item, openSeq: state.openSeq };
|
||||||
|
el.sourceTextValue.value = item.text;
|
||||||
|
el.sourceTextStatus.textContent = "";
|
||||||
|
el.sourceTextPreview.replaceChildren();
|
||||||
|
el.sourceTextPanel.showModal();
|
||||||
|
el.sourceTextValue.focus();
|
||||||
|
await previewPositionedText();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
|
} else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
|
||||||
if (item.type === "PathObject") {
|
if (item.type === "PathObject") {
|
||||||
el.shapeWidth.focus();
|
el.shapeWidth.focus();
|
||||||
@@ -2114,6 +2182,22 @@ async function editCanvasObject(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function previewPositionedText() {
|
||||||
|
const request = state.sourceText, value = el.sourceTextValue.value;
|
||||||
|
if (!request || !el.sourceTextPanel.open) return;
|
||||||
|
try {
|
||||||
|
const svg = await callWASM("ofdgoPreviewPositionedText", request.item.index, request.item.id, value);
|
||||||
|
if (state.sourceText !== request || request.openSeq !== state.openSeq || el.sourceTextValue.value !== value) return;
|
||||||
|
el.sourceTextPreview.replaceChildren(parseSVG(svg));
|
||||||
|
el.sourceTextStatus.textContent = "";
|
||||||
|
} catch (error) {
|
||||||
|
if (state.sourceText === request && request.openSeq === state.openSeq && el.sourceTextValue.value === value) {
|
||||||
|
el.sourceTextPreview.replaceChildren();
|
||||||
|
el.sourceTextStatus.textContent = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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, { ...page, objects: scope ? scope.objects : page.objects.filter(item => state.renderAnnotations || item.type !== "Annotation") }, surface);
|
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : page.objects.filter(item => state.renderAnnotations || item.type !== "Annotation") }, surface);
|
||||||
@@ -2714,6 +2798,8 @@ async function changeDocument(name, item, ...args) {
|
|||||||
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;
|
||||||
|
} else if (el.sourceTextPanel.open) {
|
||||||
|
el.sourceTextStatus.textContent = err.message;
|
||||||
} else if (el.objectBoundsPanel.open) {
|
} else if (el.objectBoundsPanel.open) {
|
||||||
el.objectBoundsStatus.textContent = err.message;
|
el.objectBoundsStatus.textContent = err.message;
|
||||||
} else if (el.outlinePanel.open) {
|
} else if (el.outlinePanel.open) {
|
||||||
@@ -3275,6 +3361,7 @@ async function openOFD(file) {
|
|||||||
el.importPanel.close();
|
el.importPanel.close();
|
||||||
el.batchPagesPanel.close();
|
el.batchPagesPanel.close();
|
||||||
el.objectStylePanel.close();
|
el.objectStylePanel.close();
|
||||||
|
el.sourceTextPanel.close();
|
||||||
el.objectBoundsPanel.close();
|
el.objectBoundsPanel.close();
|
||||||
el.outlinePanel.close();
|
el.outlinePanel.close();
|
||||||
updateControls();
|
updateControls();
|
||||||
@@ -7077,7 +7164,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? disabled : el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? disabled : el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
||||||
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
||||||
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
||||||
|| !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
|| !(item.type === "TextObject" && !item.scoped && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : 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 === "Annotation" ? "进入注解" : "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
el.editObjectButton.title = canEditObject(item, "enter") ? item?.type === "Annotation" ? "进入注解" : "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
||||||
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
||||||
|
|||||||
@@ -1186,10 +1186,12 @@ export class CanvasEditor {
|
|||||||
input.setAttribute("aria-label", "编辑文字");
|
input.setAttribute("aria-label", "编辑文字");
|
||||||
input.setAttribute("role", "textbox");
|
input.setAttribute("role", "textbox");
|
||||||
input.setAttribute("aria-multiline", "true");
|
input.setAttribute("aria-multiline", "true");
|
||||||
|
input.setAttribute("dir", "auto");
|
||||||
input.contentEditable = "true";
|
input.contentEditable = "true";
|
||||||
input.spellcheck = false;
|
input.spellcheck = false;
|
||||||
for (const text of item.text.split("\n")) {
|
for (const text of item.text.split("\n")) {
|
||||||
const paragraph = document.createElement("div");
|
const paragraph = document.createElement("div");
|
||||||
|
paragraph.setAttribute("dir", "auto");
|
||||||
if (text) paragraph.textContent = text;
|
if (text) paragraph.textContent = text;
|
||||||
else paragraph.append(document.createElement("br"));
|
else paragraph.append(document.createElement("br"));
|
||||||
input.append(paragraph);
|
input.append(paragraph);
|
||||||
@@ -1206,6 +1208,7 @@ export class CanvasEditor {
|
|||||||
minHeight: autoSize ? "0px" : `${frame.height * PX_PER_MM + 4}px`,
|
minHeight: autoSize ? "0px" : `${frame.height * PX_PER_MM + 4}px`,
|
||||||
transform: `matrix(${a},${b},${c},${d},${(e + a * left) * PX_PER_MM},${(f + b * left) * PX_PER_MM})`,
|
transform: `matrix(${a},${b},${c},${d},${(e + a * left) * PX_PER_MM},${(f + b * left) * PX_PER_MM})`,
|
||||||
whiteSpace: item.wrap ? "pre-wrap" : "pre",
|
whiteSpace: item.wrap ? "pre-wrap" : "pre",
|
||||||
|
unicodeBidi: "plaintext",
|
||||||
fontFamily: `"${face.family}"`, fontSize: `${item.size * PX_PER_MM}px`,
|
fontFamily: `"${face.family}"`, fontSize: `${item.size * PX_PER_MM}px`,
|
||||||
lineHeight: item.paragraphHeight || item.lineHeight ? `${(item.paragraphHeight || item.lineHeight) * PX_PER_MM}px` : "normal", color: item.color,
|
lineHeight: item.paragraphHeight || item.lineHeight ? `${(item.paragraphHeight || item.lineHeight) * PX_PER_MM}px` : "normal", color: item.color,
|
||||||
textAlign: item.align || "left",
|
textAlign: item.align || "left",
|
||||||
|
|||||||
+65
-4
@@ -184,6 +184,7 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoReadAnnotation", readAnnotation)
|
registerCallback("ofdgoReadAnnotation", readAnnotation)
|
||||||
registerCallback("ofdgoInsertInk", insertInk)
|
registerCallback("ofdgoInsertInk", insertInk)
|
||||||
registerCallback("ofdgoStyleObjects", styleObjects)
|
registerCallback("ofdgoStyleObjects", styleObjects)
|
||||||
|
registerCallback("ofdgoPreviewPositionedText", previewPositionedText)
|
||||||
registerCallback("ofdgoMoveOutline", moveOutline)
|
registerCallback("ofdgoMoveOutline", moveOutline)
|
||||||
registerCallback("ofdgoCaptureStyle", captureStyle)
|
registerCallback("ofdgoCaptureStyle", captureStyle)
|
||||||
registerCallback("ofdgoGroupObjects", groupObjects)
|
registerCallback("ofdgoGroupObjects", groupObjects)
|
||||||
@@ -1702,6 +1703,7 @@ func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[st
|
|||||||
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
|
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
|
||||||
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "ungroup": capability.Ungroup, "stretch": capability.Stretch, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "ungroup": capability.Ungroup, "stretch": capability.Stretch, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||||
result["textContent"] = capability.TextContent
|
result["textContent"] = capability.TextContent
|
||||||
|
result["rewriteText"] = capability.RewriteText
|
||||||
if missing := capability.MissingGlyphs; missing != nil {
|
if missing := capability.MissingGlyphs; missing != nil {
|
||||||
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
||||||
}
|
}
|
||||||
@@ -1711,6 +1713,16 @@ func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[st
|
|||||||
// editorAppearance 提供对象自身透明度及有效绘制外观,不混入父对象透明度
|
// editorAppearance 提供对象自身透明度及有效绘制外观,不混入父对象透明度
|
||||||
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
|
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
|
||||||
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
|
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
|
||||||
|
links := 0
|
||||||
|
for _, action := range object.Actions() {
|
||||||
|
if action.Event == "CLICK" && (action.URI != nil || action.Goto != nil) {
|
||||||
|
links++
|
||||||
|
if data, err := json.Marshal(action); err == nil {
|
||||||
|
item["link"] = string(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item["linkCount"] = links
|
||||||
var alpha *int
|
var alpha *int
|
||||||
var fill, stroke *ofdgo.FillColor
|
var fill, stroke *ofdgo.FillColor
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
@@ -1945,7 +1957,7 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
if len(indexes) != 1 {
|
if len(indexes) != 1 {
|
||||||
return fmt.Errorf("text layout requires one member")
|
return fmt.Errorf("text layout requires one member")
|
||||||
}
|
}
|
||||||
layout := ofdgo.TextLayout{Wrap: args[6].Bool(), Align: args[7].String(), LineHeight: args[8].Float(), LetterSpacing: args[9].Float(), LeftIndent: args[10].Float(), RightIndent: args[11].Float(), FirstLineIndent: args[12].Float()}
|
layout := ofdgo.TextLayout{AutoBidi: true, Wrap: args[6].Bool(), Align: args[7].String(), LineHeight: args[8].Float(), LetterSpacing: args[9].Float(), LeftIndent: args[10].Float(), RightIndent: args[11].Float(), FirstLineIndent: args[12].Float()}
|
||||||
if !args[4].IsNull() {
|
if !args[4].IsNull() {
|
||||||
return currentEditor.ResizeCompositeTextFrame(page, path, indexes[0], args[4].Float(), args[5].Float(), layout)
|
return currentEditor.ResizeCompositeTextFrame(page, path, indexes[0], args[4].Float(), args[5].Float(), layout)
|
||||||
}
|
}
|
||||||
@@ -2224,6 +2236,40 @@ func previewText(args []js.Value) (any, error) {
|
|||||||
return map[string]any{"runs": runs, "scale": scale, "weight": text.Weight, "italic": text.Italic}, nil
|
return map[string]any{"runs": runs, "scale": scale, "weight": text.Weight, "italic": text.Italic}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// previewPositionedText 预览保留定位的文字替换,不修改编辑器或撤销记录
|
||||||
|
// 入参: args 页面、对象标识和内容
|
||||||
|
// 返回: any SVG预览, error 错误信息
|
||||||
|
func previewPositionedText(args []js.Value) (any, error) {
|
||||||
|
object, err := currentEditor.Object(args[0].Int(), args[1].String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if object.Type != "TextObject" {
|
||||||
|
return nil, fmt.Errorf("object is not text")
|
||||||
|
}
|
||||||
|
if err := currentEditor.RewriteText(&object.TextObject, args[2].String()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
measured, err := currentSession.Renderer.MeasureObject(object, ofdgo.MeasureOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
box := measured.Bounds
|
||||||
|
margin := object.TextObject.Size / 4
|
||||||
|
box.X, box.Y, box.W, box.H = box.X-margin, box.Y-margin, box.W+2*margin, box.H+2*margin
|
||||||
|
boundary, err := ofdgo.ParseBox(object.TextObject.Boundary)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
object.TextObject.Boundary = fmt.Sprintf("%g %g %g %g", boundary.X-box.X, boundary.Y-box.Y, boundary.W, boundary.H)
|
||||||
|
page := &ofdgo.PageContent{Area: ofdgo.PageArea{PhysicalBox: fmt.Sprintf("0 0 %g %g", box.W, box.H)}, Content: ofdgo.Content{Layer: []ofdgo.Layer{{Objects: []ofdgo.GraphicObject{object}}}}}
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := currentSession.Renderer.RenderToSVG(page, &output); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return output.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// updateText 修改文字,null内容保留定位且允许替换字体,null字体数据和颜色沿用对象属性
|
// updateText 修改文字,null内容保留定位且允许替换字体,null字体数据和颜色沿用对象属性
|
||||||
// 入参: args 页码、对象标识、内容、字体数据、字号和颜色
|
// 入参: args 页码、对象标识、内容、字体数据、字号和颜色
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
@@ -2258,7 +2304,10 @@ func updateText(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
if !args[2].IsNull() {
|
if !args[2].IsNull() {
|
||||||
if args[4].Float() == object.TextObject.Size {
|
if args[4].Float() == object.TextObject.Size {
|
||||||
style := ofdgo.TextStyle{Font: object.TextObject.Font}
|
var style ofdgo.TextStyle
|
||||||
|
if !args[3].IsNull() {
|
||||||
|
style.Font = object.TextObject.Font
|
||||||
|
}
|
||||||
if !args[5].IsNull() {
|
if !args[5].IsNull() {
|
||||||
var fill ofdgo.FillColor
|
var fill ofdgo.FillColor
|
||||||
if err := setEditorColor(&fill, args[5].String()); err != nil {
|
if err := setEditorColor(&fill, args[5].String()); err != nil {
|
||||||
@@ -2407,7 +2456,7 @@ func insertText(args []js.Value) (any, error) {
|
|||||||
Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H),
|
Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H),
|
||||||
Font: fontID, Size: args[5].Float(),
|
Font: fontID, Size: args[5].Float(),
|
||||||
}}
|
}}
|
||||||
if err := currentEditor.LayoutText(&object.TextObject, args[1].String(), ofdgo.TextLayout{Wrap: args[8].Bool(), Align: args[9].String(), LineHeight: args[10].Float(), LetterSpacing: args[11].Float(), LeftIndent: args[12].Float(), RightIndent: args[13].Float(), FirstLineIndent: args[14].Float()}); err != nil {
|
if err := currentEditor.LayoutText(&object.TextObject, args[1].String(), ofdgo.TextLayout{AutoBidi: true, Wrap: args[8].Bool(), Align: args[9].String(), LineHeight: args[10].Float(), LetterSpacing: args[11].Float(), LeftIndent: args[12].Float(), RightIndent: args[13].Float(), FirstLineIndent: args[14].Float()}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := setTextColor(&object.TextObject, args[6].String()); err != nil {
|
if err := setTextColor(&object.TextObject, args[6].String()); err != nil {
|
||||||
@@ -2438,7 +2487,7 @@ func layoutText(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
value, _ := object.TextObject.TextLayout()
|
value, _ := object.TextObject.TextLayout()
|
||||||
if err := currentEditor.LayoutText(&object.TextObject, value, ofdgo.TextLayout{Wrap: args[4].Bool(), Align: args[5].String(), LineHeight: args[6].Float(), LetterSpacing: args[7].Float(), LeftIndent: args[8].Float(), RightIndent: args[9].Float(), FirstLineIndent: args[10].Float()}); err != nil {
|
if err := currentEditor.LayoutText(&object.TextObject, value, ofdgo.TextLayout{AutoBidi: true, Wrap: args[4].Bool(), Align: args[5].String(), LineHeight: args[6].Float(), LetterSpacing: args[7].Float(), LeftIndent: args[8].Float(), RightIndent: args[9].Float(), FirstLineIndent: args[10].Float()}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
revision := currentEditor.Revision()
|
revision := currentEditor.Revision()
|
||||||
@@ -2938,6 +2987,13 @@ func changeOutline(args []js.Value) (any, error) {
|
|||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func styleObjects(args []js.Value) (any, error) {
|
func styleObjects(args []js.Value) (any, error) {
|
||||||
style := objectStyle(args[2])
|
style := objectStyle(args[2])
|
||||||
|
linkField := args[2].Get("link")
|
||||||
|
var link *ofdgo.AnnotationLink
|
||||||
|
if !linkField.IsUndefined() && !linkField.IsNull() {
|
||||||
|
if err := json.Unmarshal([]byte(linkField.String()), &link); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var border *ofdgo.ImageBorderStyle
|
var border *ofdgo.ImageBorderStyle
|
||||||
if field := args[2].Get("borderStyle"); !field.IsUndefined() {
|
if field := args[2].Get("borderStyle"); !field.IsUndefined() {
|
||||||
if err := json.Unmarshal([]byte(field.String()), &border); err != nil {
|
if err := json.Unmarshal([]byte(field.String()), &border); err != nil {
|
||||||
@@ -2967,6 +3023,11 @@ func styleObjects(args []js.Value) (any, error) {
|
|||||||
return changeObjects(func() error {
|
return changeObjects(func() error {
|
||||||
return currentEditor.Transaction(func(editor *ofdgo.Editor) error {
|
return currentEditor.Transaction(func(editor *ofdgo.Editor) error {
|
||||||
page, ids := args[0].Int(), stringsFromJS(args[1])
|
page, ids := args[0].Int(), stringsFromJS(args[1])
|
||||||
|
if !linkField.IsUndefined() {
|
||||||
|
if err := editor.SetObjectLink(page, ids, link); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if border != nil {
|
if border != nil {
|
||||||
if err := resolveStylePaint((*ofdgo.FillColor)(border.Color)); err != nil {
|
if err := resolveStylePaint((*ofdgo.FillColor)(border.Color)); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+17
-1
@@ -149,6 +149,17 @@ func backendName(backend Backend) string {
|
|||||||
func WithRenderBackends(backends RenderBackends) RendererOption {
|
func WithRenderBackends(backends RenderBackends) RendererOption {
|
||||||
return func(r *Renderer) {
|
return func(r *Renderer) {
|
||||||
fontsChanged := !sameBackend(r.backends.Fonts, backends.Fonts)
|
fontsChanged := !sameBackend(r.backends.Fonts, backends.Fonts)
|
||||||
|
for _, pair := range [][2]Backend{
|
||||||
|
{r.backends.FontResources, backends.FontResources}, {r.backends.Fonts, backends.Fonts},
|
||||||
|
{r.backends.Geometry, backends.Geometry}, {r.backends.Compiler, backends.Compiler},
|
||||||
|
{r.backends.Raster, backends.Raster}, {r.backends.SVG, backends.SVG},
|
||||||
|
{r.backends.PDF, backends.PDF}, {r.backends.EPS, backends.EPS},
|
||||||
|
} {
|
||||||
|
if !sameBackend(pair[0], pair[1]) {
|
||||||
|
r.backendStates = make(map[any]any)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
r.backends = backends
|
r.backends = backends
|
||||||
if fontsChanged {
|
if fontsChanged {
|
||||||
r.resetFontBackendCache()
|
r.resetFontBackendCache()
|
||||||
@@ -202,7 +213,12 @@ func (r *Renderer) Backends() RenderBackends {
|
|||||||
// 入参: backend 光栅后端,nil禁用图像输出
|
// 入参: backend 光栅后端,nil禁用图像输出
|
||||||
// 返回: RendererOption 渲染选项
|
// 返回: RendererOption 渲染选项
|
||||||
func WithRasterBackend(backend RasterBackend) RendererOption {
|
func WithRasterBackend(backend RasterBackend) RendererOption {
|
||||||
return func(r *Renderer) { r.backends.Raster = backend }
|
return func(r *Renderer) {
|
||||||
|
if !sameBackend(r.backends.Raster, backend) {
|
||||||
|
r.backendStates = make(map[any]any)
|
||||||
|
}
|
||||||
|
r.backends.Raster = backend
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompilePage 编译与后端无关的光栅页面,可交给任意RasterBackend绘制
|
// CompilePage 编译与后端无关的光栅页面,可交给任意RasterBackend绘制
|
||||||
|
|||||||
+2
-1
@@ -28,7 +28,7 @@ var (
|
|||||||
ErrEncryptionPolicyRequired = errors.New("explicit encryption policy required")
|
ErrEncryptionPolicyRequired = errors.New("explicit encryption policy required")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Credentials 解密凭据,UserName为空时尝试匹配用户;证书采用DER编码
|
// Credentials 解密凭据,UserName用于筛选接收者,留空自动匹配,不参与口令派生;证书采用DER编码
|
||||||
// Password与Decrypter分别用于口令和证书方案,私钥可由外部密码设备持有
|
// Password与Decrypter分别用于口令和证书方案,私钥可由外部密码设备持有
|
||||||
type Credentials struct {
|
type Credentials struct {
|
||||||
UserName string
|
UserName string
|
||||||
@@ -86,6 +86,7 @@ type EncryptionRecipient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EncryptionOptions 设置GM/T0099加密,口令与证书方案二选一
|
// EncryptionOptions 设置GM/T0099加密,口令与证书方案二选一
|
||||||
|
// UserName标记口令接收者,不参与口令派生,留空使用User
|
||||||
// 口令方案应使用足够长的随机口令;标准密钥派生不提供慢速口令哈希
|
// 口令方案应使用足够长的随机口令;标准密钥派生不提供慢速口令哈希
|
||||||
type EncryptionOptions struct {
|
type EncryptionOptions struct {
|
||||||
Password []byte
|
Password []byte
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ func (e *Editor) SetFontFS(fsys ...fs.FS) {
|
|||||||
func (e *Editor) SetPageCompiler(compiler PageCompiler) {
|
func (e *Editor) SetPageCompiler(compiler PageCompiler) {
|
||||||
e.backends.Compiler = compiler
|
e.backends.Compiler = compiler
|
||||||
if e.fontRenderer != nil {
|
if e.fontRenderer != nil {
|
||||||
e.fontRenderer.backends.Compiler = compiler
|
WithRenderBackends(e.backends)(e.fontRenderer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -526,7 +526,7 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve
|
|||||||
beforeOrigins[ids[i]], afterOrigins[ids[i]] = origin, next
|
beforeOrigins[ids[i]], afterOrigins[ids[i]] = origin, next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if reflect.DeepEqual(before, after) {
|
if reflect.DeepEqual(before, after) && len(afterOrigins) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
apply := func(e *Editor, objects []GraphicObject, origins map[string]*editorObjectOrigin) {
|
apply := func(e *Editor, objects []GraphicObject, origins map[string]*editorObjectOrigin) {
|
||||||
|
|||||||
+270
-100
@@ -56,6 +56,7 @@ type editorPageImport struct {
|
|||||||
defaultCS string
|
defaultCS string
|
||||||
attachments map[string][]byte
|
attachments map[string][]byte
|
||||||
copyPage bool
|
copyPage bool
|
||||||
|
strictLinks bool
|
||||||
progress editorProgress
|
progress editorProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +70,152 @@ type PageImportOptions struct {
|
|||||||
OnProgress func(stage string, completed, total int) error
|
OnProgress func(stage string, completed, total int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ObjectImportOptions 跨文档对象迁移选项,位移单位为毫米
|
||||||
|
// AllowDecrypted显式允许将加密来源导入未加密目标,进度回调返回错误时撤销整个操作
|
||||||
|
type ObjectImportOptions struct {
|
||||||
|
DX, DY float64
|
||||||
|
AllowDecrypted bool
|
||||||
|
OnProgress func(stage string, completed, total int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportObjects 迁移选区及引用资源,不改变来源与目标页面结构,提交一次撤销记录
|
||||||
|
// 保留对象原文与图层继承,同页链接映射到目标页,无法迁移的跨页链接明确报错
|
||||||
|
// 返回后可关闭来源阅读器,后续修改来源不影响副本
|
||||||
|
// 入参: source 来源编辑器, sourcePage 来源页, ids 对象标识, page 目标页, options 迁移选项
|
||||||
|
// 返回: []string 新对象标识, error 错误信息
|
||||||
|
func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, page int, options ObjectImportOptions) ([]string, error) {
|
||||||
|
if !finite(options.DX) || !finite(options.DY) {
|
||||||
|
return nil, fmt.Errorf("import requires finite offsets")
|
||||||
|
}
|
||||||
|
if source.encryption != nil && e.encryption == nil && !options.AllowDecrypted {
|
||||||
|
return nil, ErrEncryptionPolicyRequired
|
||||||
|
}
|
||||||
|
objects, err := source.Objects(sourcePage, ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
target, err := e.page(page)
|
||||||
|
if err != nil || len(objects) == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if source == e {
|
||||||
|
return e.CopyObjects(page, objects, options.DX, options.DY)
|
||||||
|
}
|
||||||
|
reader, err := source.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
doc, err := reader.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ref := doc.Pages.Page[sourcePage]
|
||||||
|
progress := editorProgress(options.OnProgress)
|
||||||
|
base, migration, err := e.prepareImport(reader, doc, map[string]bool{ref.ID: true}, false, progress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
migration.ids[ref.ID], migration.strictLinks = target.ID, true
|
||||||
|
pageName := reader.ResPath(ref.BaseLoc)
|
||||||
|
content, err := reader.PageContentByIndex(sourcePage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, value := range content.PageRes {
|
||||||
|
name, err := migration.resolve(pageName, "", value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := migration.resourceIndex(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layers := make([][]byte, len(objects))
|
||||||
|
for i, object := range objects {
|
||||||
|
if err := progress.report("objects", i, len(objects)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
var attrs ofdAttrs
|
||||||
|
if origin := object.origin; origin != nil {
|
||||||
|
data, err = editorXMLObject(origin.data, origin.node, origin.object, object)
|
||||||
|
if err == nil {
|
||||||
|
data, err = editorXMLStandalone(data, origin.node)
|
||||||
|
}
|
||||||
|
for parent := origin.node.parent; parent != nil; parent = parent.parent {
|
||||||
|
if parent.name.Local == "Layer" {
|
||||||
|
attrs.add("DrawParam", parent.attr("DrawParam"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err = editorObjectXML(object)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err = editorXMLContainer("Layer", attrs, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
layers[i], err = migration.encode(editorImportEntry{name: pageName, data: data}, root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := migration.encodeResources(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
delete(migration.files, resource.name)
|
||||||
|
}
|
||||||
|
next, err := migration.merge(base, nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var copied []string
|
||||||
|
err = e.Transaction(func(editor *Editor) error {
|
||||||
|
if err := progress.report("commit", 0, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
editor.bindImportResources()
|
||||||
|
editor.source, editor.maxID = next, migration.maximum
|
||||||
|
next.idsReady = true
|
||||||
|
imported := make([]GraphicObject, len(layers))
|
||||||
|
for i, data := range layers {
|
||||||
|
data, err := editorXMLContainer("Content", nil, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var content Content
|
||||||
|
if err := xml.Unmarshal(data, &content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
layer := content.Layer[0]
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object := layer.Objects[0]
|
||||||
|
imported[i], err = editor.copiedObjectStyle(object, layer.DrawParam)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
imported[i].TextObject.layout = objects[i].TextObject.layout
|
||||||
|
imported[i].state = objects[i].state
|
||||||
|
imported[i].CompositeGraphicUnit.states = remapCompositeStates(objects[i].CompositeGraphicUnit.states, migration.ids)
|
||||||
|
imported[i].origin = &editorObjectOrigin{editor: editor, data: data, node: root.child("Layer").children[0], object: object}
|
||||||
|
}
|
||||||
|
copied, err = editor.CopyObjects(page, imported, options.DX, options.DY)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return copied, err
|
||||||
|
}
|
||||||
|
|
||||||
// ImportPages 将来源主文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
// ImportPages 将来源主文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
||||||
// 页面索引从0开始,at可等于当前页数;复制关联模板、注释、签章外观和实际引用的资源
|
// 页面索引从0开始,at可等于当前页数;复制关联模板、注释、签章外观和实际引用的资源
|
||||||
// 不导入来源元数据和目录,指向未选页面的跳转被移除;签名数据仅保留原始凭据,不代表合并后文档有效
|
// 不导入来源元数据和目录,指向未选页面的跳转被移除;签名数据仅保留原始凭据,不代表合并后文档有效
|
||||||
@@ -117,56 +264,10 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
progress := editorProgress(options.OnProgress)
|
progress := editorProgress(options.OnProgress)
|
||||||
maximum, err := e.sourceMaxID(progress)
|
base, m, err := e.prepareImport(source, doc, selected, copyPage, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
base := e.source
|
|
||||||
if base == nil {
|
|
||||||
base, err = e.importBase()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m := &editorPageImport{reader: source, doc: doc, directory: e.packageDirectory(), target: base.reader, maximum: maximum, copyPage: copyPage, progress: progress,
|
|
||||||
paths: make(map[string]string), evidence: make(map[string]string),
|
|
||||||
ids: make(map[string]string), pages: selected, files: make(map[string][]byte), resources: make(map[string]editorImportEntry), used: make(map[string][]byte), templates: make(map[string]TemplatePage), templateParts: make(map[string][]byte), attachments: make(map[string][]byte)}
|
|
||||||
for _, resource := range e.resources {
|
|
||||||
m.files[resource.name] = nil
|
|
||||||
}
|
|
||||||
m.documentXML, err = m.readFile(source.ResPath(source.OFD.DocBody[0].DocRoot))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m.documentRoot, err = parseEditorXML(m.documentXML)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if copyPage {
|
|
||||||
for _, page := range doc.Pages.Page {
|
|
||||||
if !selected[page.ID] {
|
|
||||||
m.ids[page.ID] = page.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, name := range append(slices.Clone(doc.CommonData.PublicRes), doc.CommonData.DocumentRes...) {
|
|
||||||
if err := m.resourceIndex(source.ResPath(name)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, template := range doc.CommonData.TemplatePage {
|
|
||||||
m.templates[template.ID] = template
|
|
||||||
}
|
|
||||||
if doc.CommonData.DefaultCS != 0 {
|
|
||||||
m.defaultCS, err = m.reference(strconv.Itoa(doc.CommonData.DefaultCS))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else if !e.sourceRGB() {
|
|
||||||
m.defaultCS = m.id("defaultRGB")
|
|
||||||
m.used["defaultRGB"] = []byte(`<ofd:ColorSpace ID="` + m.defaultCS + `" Type="RGB" BitsPerComponent="8"/>`)
|
|
||||||
m.resources["defaultRGB"] = editorImportEntry{group: "ColorSpaces"}
|
|
||||||
}
|
|
||||||
refs := make([]Page, len(indexes))
|
refs := make([]Page, len(indexes))
|
||||||
ids := make([]string, len(indexes))
|
ids := make([]string, len(indexes))
|
||||||
for i, index := range indexes {
|
for i, index := range indexes {
|
||||||
@@ -209,30 +310,9 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !copyPage {
|
if !copyPage {
|
||||||
var resourceContent []byte
|
if err := m.encodeResources(); err != nil {
|
||||||
for _, group := range []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"} {
|
|
||||||
var entries []byte
|
|
||||||
for _, id := range slices.Sorted(maps.Keys(m.used)) {
|
|
||||||
if m.resources[id].group == group {
|
|
||||||
entries = append(entries, m.used[id]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(entries) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
encoded, err := editorXMLContainer(group, nil, entries)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resourceContent = append(resourceContent, encoded...)
|
|
||||||
}
|
|
||||||
resources, err := editorXMLContainer("Res", nil, resourceContent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(resourceContent) != 0 {
|
|
||||||
m.resourceXML = resources
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
delete(m.files, resource.name)
|
delete(m.files, resource.name)
|
||||||
@@ -281,19 +361,7 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
if err := progress.report("commit", 0, 0); err != nil {
|
if err := progress.report("commit", 0, 0); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e.source == nil {
|
e.bindImportResources()
|
||||||
for i := range e.resources {
|
|
||||||
resource := &e.resources[i]
|
|
||||||
if resource.font != nil {
|
|
||||||
resource.font = cloneEditorData(resource.font)
|
|
||||||
resource.font.FontFile = "/" + resource.name
|
|
||||||
}
|
|
||||||
if resource.image != nil {
|
|
||||||
resource.image = cloneEditorData(resource.image)
|
|
||||||
resource.image.MediaFile = "/" + resource.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.source, e.maxID = next, m.maximum
|
e.source, e.maxID = next, m.maximum
|
||||||
next.idsReady = true
|
next.idsReady = true
|
||||||
e.outlines = outlines
|
e.outlines = outlines
|
||||||
@@ -320,6 +388,111 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bindImportResources 将新建文档资源路径绑定到包根,保持迁移前后的解析一致
|
||||||
|
func (e *Editor) bindImportResources() {
|
||||||
|
if e.source != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range e.resources {
|
||||||
|
resource := &e.resources[i]
|
||||||
|
if resource.font != nil {
|
||||||
|
resource.font = cloneEditorData(resource.font)
|
||||||
|
resource.font.FontFile = "/" + resource.name
|
||||||
|
}
|
||||||
|
if resource.image != nil {
|
||||||
|
resource.image = cloneEditorData(resource.image)
|
||||||
|
resource.image.MediaFile = "/" + resource.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareImport 建立页面和对象迁移共用的资源图,不修改目标编辑器
|
||||||
|
// 入参: source 来源, doc 来源文档, selected 页面映射范围, copyPage 是否同文档, progress 进度
|
||||||
|
// 返回: *editorSource 目标基础, *editorPageImport 资源图, error 错误信息
|
||||||
|
func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[string]bool, copyPage bool, progress editorProgress) (*editorSource, *editorPageImport, error) {
|
||||||
|
maximum, err := e.sourceMaxID(progress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
base := e.source
|
||||||
|
if base == nil {
|
||||||
|
base, err = e.importBase()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m := &editorPageImport{reader: source, doc: doc, directory: e.packageDirectory(), target: base.reader, maximum: maximum, copyPage: copyPage, progress: progress,
|
||||||
|
paths: make(map[string]string), evidence: make(map[string]string), ids: make(map[string]string), pages: selected,
|
||||||
|
files: make(map[string][]byte), resources: make(map[string]editorImportEntry), used: make(map[string][]byte),
|
||||||
|
templates: make(map[string]TemplatePage), templateParts: make(map[string][]byte), attachments: make(map[string][]byte)}
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
m.files[resource.name] = nil
|
||||||
|
}
|
||||||
|
m.documentXML, err = m.readFile(source.ResPath(source.OFD.DocBody[0].DocRoot))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
m.documentRoot, err = parseEditorXML(m.documentXML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if copyPage {
|
||||||
|
for _, page := range doc.Pages.Page {
|
||||||
|
if !selected[page.ID] {
|
||||||
|
m.ids[page.ID] = page.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, name := range append(slices.Clone(doc.CommonData.PublicRes), doc.CommonData.DocumentRes...) {
|
||||||
|
if err := m.resourceIndex(source.ResPath(name)); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, template := range doc.CommonData.TemplatePage {
|
||||||
|
m.templates[template.ID] = template
|
||||||
|
}
|
||||||
|
if doc.CommonData.DefaultCS != 0 {
|
||||||
|
m.defaultCS, err = m.reference(strconv.Itoa(doc.CommonData.DefaultCS))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
} else if !e.sourceRGB() {
|
||||||
|
m.defaultCS = m.id("defaultRGB")
|
||||||
|
m.used["defaultRGB"] = []byte(`<ofd:ColorSpace ID="` + m.defaultCS + `" Type="RGB" BitsPerComponent="8"/>`)
|
||||||
|
m.resources["defaultRGB"] = editorImportEntry{group: "ColorSpaces"}
|
||||||
|
}
|
||||||
|
return base, m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeResources 编码本次迁移实际引用的资源,不包含未使用的资源定义
|
||||||
|
// 返回: error 编码错误
|
||||||
|
func (m *editorPageImport) encodeResources() error {
|
||||||
|
var content []byte
|
||||||
|
ids := slices.Sorted(maps.Keys(m.used))
|
||||||
|
for _, group := range []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"} {
|
||||||
|
var entries []byte
|
||||||
|
for _, id := range ids {
|
||||||
|
if m.resources[id].group == group {
|
||||||
|
entries = append(entries, m.used[id]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(entries) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
encoded, err := editorXMLContainer(group, nil, entries)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content = append(content, encoded...)
|
||||||
|
}
|
||||||
|
if len(content) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
m.resourceXML, err = editorXMLContainer("Res", nil, content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// readFile 分块读取导入条目,允许资源解压过程中取消,不共享来源内存
|
// readFile 分块读取导入条目,允许资源解压过程中取消,不共享来源内存
|
||||||
// 入参: name 包内路径
|
// 入参: name 包内路径
|
||||||
// 返回: []byte 条目内容, error 错误信息
|
// 返回: []byte 条目内容, error 错误信息
|
||||||
@@ -738,6 +911,9 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
|||||||
bookmarks[bookmark.Name] = bookmark.Dest
|
bookmarks[bookmark.Name] = bookmark.Dest
|
||||||
}
|
}
|
||||||
if dest := gotoDest(&action, bookmarks); !m.copyPage && (dest == nil || !m.pages[dest.PageID]) {
|
if dest := gotoDest(&action, bookmarks); !m.copyPage && (dest == nil || !m.pages[dest.PageID]) {
|
||||||
|
if m.strictLinks {
|
||||||
|
return nil, fmt.Errorf("object link targets a page outside the imported selection")
|
||||||
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1073,20 +1249,24 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
|||||||
for _, id := range slices.Sorted(maps.Keys(m.templateParts)) {
|
for _, id := range slices.Sorted(maps.Keys(m.templateParts)) {
|
||||||
content = append(content, m.templateParts[id]...)
|
content = append(content, m.templateParts[id]...)
|
||||||
}
|
}
|
||||||
data = editorPatchXML(data, []editorXMLPatch{{position, position, content}})
|
patches := []editorXMLPatch{{position, position, content}}
|
||||||
root, err = parseEditorXML(data)
|
patch := editorXMLPatch{common.open, common.open, editorXMLText("MaxUnitID", strconv.Itoa(m.maximum))}
|
||||||
if err != nil {
|
if maximum := common.child("MaxUnitID"); maximum != nil {
|
||||||
return nil, err
|
patch = editorXMLContent(data, maximum, []byte(strconv.Itoa(m.maximum)))
|
||||||
}
|
}
|
||||||
|
patches = append(patches, patch)
|
||||||
pages := root.child("Pages")
|
pages := root.child("Pages")
|
||||||
if pages == nil {
|
if pages == nil {
|
||||||
return nil, fmt.Errorf("document has no Pages")
|
return nil, fmt.Errorf("document has no Pages")
|
||||||
}
|
}
|
||||||
content = bytes.Clone(m.pageParts)
|
if len(m.pageParts) != 0 {
|
||||||
if pages.open != pages.end {
|
if pages.open == pages.end {
|
||||||
content = append(bytes.Clone(data[pages.open:pages.close]), content...)
|
patches = append(patches, editorXMLContent(data, pages, m.pageParts))
|
||||||
|
} else {
|
||||||
|
patches = append(patches, editorXMLPatch{pages.close, pages.close, m.pageParts})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, pages, content)})
|
data = editorPatchXML(data, patches)
|
||||||
if len(annotations) != 0 {
|
if len(annotations) != 0 {
|
||||||
loc, err := mergeImportIndex(base.reader, files, base.document.Annotations, path.Join(m.directory, "Annotations.xml"), "Annotations", annotations)
|
loc, err := mergeImportIndex(base.reader, files, base.document.Annotations, path.Join(m.directory, "Annotations.xml"), "Annotations", annotations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1117,16 +1297,6 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root, err = parseEditorXML(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
common = root.child("CommonData")
|
|
||||||
patch := editorXMLPatch{common.open, common.open, editorXMLText("MaxUnitID", strconv.Itoa(m.maximum))}
|
|
||||||
if maximum := common.child("MaxUnitID"); maximum != nil {
|
|
||||||
patch = editorXMLContent(data, maximum, []byte(strconv.Itoa(m.maximum)))
|
|
||||||
}
|
|
||||||
data = editorPatchXML(data, []editorXMLPatch{patch})
|
|
||||||
files[name] = data
|
files[name] = data
|
||||||
if len(signatures) != 0 {
|
if len(signatures) != 0 {
|
||||||
loc, err := mergeImportIndex(base.reader, files, base.document.Signatures, path.Join(m.directory, "Signs", "Signatures.xml"), "Signatures", signatures)
|
loc, err := mergeImportIndex(base.reader, files, base.document.Signatures, path.Join(m.directory, "Signs", "Signatures.xml"), "Signatures", signatures)
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ type editorSourcePage struct {
|
|||||||
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
||||||
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
||||||
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
||||||
|
// RewriteText表示可保留原定位修改,具体替换仍需校验长度、字形映射和字体覆盖
|
||||||
// Ungroup表示可移除组合容器并保留内部内容,Stretch表示图片及纯图片组合可独立调整宽高
|
// Ungroup表示可移除组合容器并保留内部内容,Stretch表示图片及纯图片组合可独立调整宽高
|
||||||
type ObjectCapabilities struct {
|
type ObjectCapabilities struct {
|
||||||
Update bool
|
Update bool
|
||||||
TextContent bool
|
TextContent bool
|
||||||
|
RewriteText bool
|
||||||
Paint bool
|
Paint bool
|
||||||
ReplaceFont bool
|
ReplaceFont bool
|
||||||
ReplaceImage bool
|
ReplaceImage bool
|
||||||
@@ -504,6 +506,7 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
|||||||
origin := e.objectOrigin(id)
|
origin := e.objectOrigin(id)
|
||||||
if origin == nil {
|
if origin == nil {
|
||||||
all.TextContent = all.Reflow
|
all.TextContent = all.Reflow
|
||||||
|
all.RewriteText = object.Type == "TextObject"
|
||||||
all.Stretch = object.Type == "ImageObject"
|
all.Stretch = object.Type == "ImageObject"
|
||||||
all.ReplaceImage = object.Type == "ImageObject"
|
all.ReplaceImage = object.Type == "ImageObject"
|
||||||
all.CropImage = all.ReplaceImage
|
all.CropImage = all.ReplaceImage
|
||||||
@@ -551,7 +554,12 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
|||||||
all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject
|
all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject
|
||||||
}
|
}
|
||||||
if object.Type == "TextObject" {
|
if object.Type == "TextObject" {
|
||||||
all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0
|
all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0 && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
|
||||||
|
all.RewriteText = all.TextContent
|
||||||
|
if !all.RewriteText {
|
||||||
|
text := object.TextObject
|
||||||
|
all.RewriteText = e.RewriteText(&text, text.Text()) == nil
|
||||||
|
}
|
||||||
all.Arrange = all.Update || e.editorTextMeasurable(object.TextObject)
|
all.Arrange = all.Update || e.editorTextMeasurable(object.TextObject)
|
||||||
if _, err := object.TextObject.TextFrame(); err != nil {
|
if _, err := object.TextObject.TextFrame(); err != nil {
|
||||||
all.Reflow = false
|
all.Reflow = false
|
||||||
|
|||||||
+18
-3
@@ -236,7 +236,22 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
|
|||||||
// 返回: []byte 页面XML, error 错误信息
|
// 返回: []byte 页面XML, error 错误信息
|
||||||
func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, error) {
|
func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, error) {
|
||||||
page := e.pages[index]
|
page := e.pages[index]
|
||||||
if reflect.DeepEqual(page, *source.original) {
|
unchanged := reflect.DeepEqual(page, *source.original)
|
||||||
|
if unchanged && len(e.origins) != 0 {
|
||||||
|
for _, layer := range page.Content.Layer {
|
||||||
|
for _, object := range layer.Objects {
|
||||||
|
id := editorObjectID(object)
|
||||||
|
if origin := e.origins[id]; origin != nil && origin.node != source.nodes[id] {
|
||||||
|
unchanged = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !unchanged {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if unchanged {
|
||||||
return source.data, nil
|
return source.data, nil
|
||||||
}
|
}
|
||||||
if source.root.open == source.root.end {
|
if source.root.open == source.root.end {
|
||||||
@@ -314,11 +329,11 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
|||||||
if exists {
|
if exists {
|
||||||
nextID := editorObjectID(next)
|
nextID := editorObjectID(next)
|
||||||
origin := source.nodes[nextID]
|
origin := source.nodes[nextID]
|
||||||
if origin != nil && reflect.DeepEqual(before[nextID], next) {
|
current := e.objectOrigin(nextID)
|
||||||
|
if origin != nil && current.node == origin && reflect.DeepEqual(before[nextID], next) {
|
||||||
encoded = data[origin.start:origin.end]
|
encoded = data[origin.start:origin.end]
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
current := e.objectOrigin(nextID)
|
|
||||||
encoded, err = editorXMLObject(current.data, current.node, current.object, next)
|
encoded, err = editorXMLObject(current.data, current.node, current.object, next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
@@ -45,6 +46,136 @@ type ImageBorderStyle struct {
|
|||||||
Color *StrokeColor
|
Color *StrokeColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DrawParam 读取绘制参数,resolved为true时合并继承链,返回独立副本
|
||||||
|
// 入参: id 资源标识, resolved 是否解析继承
|
||||||
|
// 返回: *DrawParam 绘制参数, error 错误信息
|
||||||
|
func (e *Editor) DrawParam(id string, resolved bool) (*DrawParam, error) {
|
||||||
|
if resolved {
|
||||||
|
return e.editorDrawParam(id, make(map[string]bool))
|
||||||
|
}
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if resource.draw != nil && resource.draw.ID == id {
|
||||||
|
return cloneEditorData(resource.draw), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.source != nil {
|
||||||
|
if value := e.source.reader.drawParamCache[id]; value != nil {
|
||||||
|
return cloneEditorData(value), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, &EditError{Code: EditUnsupportedStyle, Err: fmt.Errorf("draw parameter %q not found", id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetObjectDrawParam 修改对象的绘制参数引用,保留局部覆盖和原文扩展,空标识恢复图层继承
|
||||||
|
// 入参: page 页面索引, ids 对象标识, id 绘制参数标识
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) SetObjectDrawParam(page int, ids []string, id string) error {
|
||||||
|
if _, err := e.editorDrawParam(id, make(map[string]bool)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objects, indexes, err := e.selectedObjects(page, ids)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
origins := make(map[string]*editorObjectOrigin)
|
||||||
|
for i, object := range objects {
|
||||||
|
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
|
||||||
|
return fmt.Errorf("object %q does not support draw parameters", ids[i])
|
||||||
|
}
|
||||||
|
origin := e.objectOrigin(ids[i])
|
||||||
|
if origin != nil && origin.node.attr("DrawParam") == id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
if origin != nil {
|
||||||
|
before, resolveErr := e.resolveEditorStyle(origin.object, e.pages[page].Content.Layer[indexes[i].layer].DrawParam)
|
||||||
|
if resolveErr != nil {
|
||||||
|
return resolveErr
|
||||||
|
}
|
||||||
|
oldXML, encodeErr := editorObjectXML(before)
|
||||||
|
if encodeErr != nil {
|
||||||
|
return encodeErr
|
||||||
|
}
|
||||||
|
comparison := object
|
||||||
|
comparison.TextObject.DrawParam, comparison.PathObject.DrawParam = "", ""
|
||||||
|
newXML, encodeErr := editorObjectXML(comparison)
|
||||||
|
if encodeErr != nil {
|
||||||
|
return encodeErr
|
||||||
|
}
|
||||||
|
data, err = editorXMLMerge(origin.data, origin.node, oldXML, newXML)
|
||||||
|
if err == nil {
|
||||||
|
data, err = editorXMLStandalone(data, origin.node)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err = editorObjectXML(object)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
|
before, encodeErr := editorXMLContainer(root.name.Local, ofdAttrs{{Name: xml.Name{Local: "DrawParam"}, Value: root.attr("DrawParam")}}, nil)
|
||||||
|
if encodeErr != nil {
|
||||||
|
return encodeErr
|
||||||
|
}
|
||||||
|
after, encodeErr := editorXMLContainer(root.name.Local, nil, nil)
|
||||||
|
if encodeErr != nil {
|
||||||
|
return encodeErr
|
||||||
|
}
|
||||||
|
data, err = editorXMLMerge(data, root, before, after)
|
||||||
|
} else {
|
||||||
|
data, err = editorXMLAttribute(data, root, "DrawParam", id)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
next := GraphicObject{Type: object.Type, state: object.state}
|
||||||
|
var target any
|
||||||
|
switch object.Type {
|
||||||
|
case "TextObject":
|
||||||
|
target = &next.TextObject
|
||||||
|
case "PathObject":
|
||||||
|
target = &next.PathObject
|
||||||
|
default:
|
||||||
|
target = &next.CompositeGraphicUnit
|
||||||
|
}
|
||||||
|
if err := xml.Unmarshal(data, target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
next, err = e.resolveEditorStyle(next, e.pages[page].Content.Layer[indexes[i].layer].DrawParam)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if next.TextObject.Font == object.TextObject.Font && next.TextObject.Size == object.TextObject.Size {
|
||||||
|
next.TextObject.layout = object.TextObject.layout
|
||||||
|
}
|
||||||
|
switch next.Type {
|
||||||
|
case "TextObject":
|
||||||
|
next.TextObject.DrawParam = id
|
||||||
|
case "PathObject":
|
||||||
|
next.PathObject.DrawParam = id
|
||||||
|
}
|
||||||
|
next.CompositeGraphicUnit.states = object.CompositeGraphicUnit.states
|
||||||
|
root, err = parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updated := &editorObjectOrigin{editor: e, data: data, node: root, object: next}
|
||||||
|
if origin != nil {
|
||||||
|
updated.page, root.parent = origin.page, origin.node.parent
|
||||||
|
} else {
|
||||||
|
content := &editorXML{name: xml.Name{Local: "Content"}}
|
||||||
|
root.parent = &editorXML{name: xml.Name{Local: "Layer"}, parent: content, children: []*editorXML{root}}
|
||||||
|
content.children = []*editorXML{root.parent}
|
||||||
|
}
|
||||||
|
objects[i], origins[ids[i]] = next, updated
|
||||||
|
}
|
||||||
|
return e.updateObjectOrigins(page, objects, true, origins)
|
||||||
|
}
|
||||||
|
|
||||||
// AddDrawParam 注册绘制参数,保留继承引用,未引用的资源不写入文档
|
// AddDrawParam 注册绘制参数,保留继承引用,未引用的资源不写入文档
|
||||||
// 入参: draw 绘制参数,ID由编辑器分配
|
// 入参: draw 绘制参数,ID由编辑器分配
|
||||||
// 返回: string 资源标识, error 错误信息
|
// 返回: string 资源标识, error 错误信息
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ func missingGlyphError(id string, characters []rune) error {
|
|||||||
// Wrap按CTM变换前的边界宽度折行,Align为left、center、right或justify
|
// Wrap按CTM变换前的边界宽度折行,Align为left、center、right或justify
|
||||||
// LineHeight为毫米单位的基线间距,0使用字体度量;LetterSpacing为字素间的附加毫米间距,可为负
|
// LineHeight为毫米单位的基线间距,0使用字体度量;LetterSpacing为字素间的附加毫米间距,可为负
|
||||||
// LeftIndent和RightIndent为左右缩进,FirstLineIndent为每段首行相对左缩进的偏移,单位为毫米
|
// LeftIndent和RightIndent为左右缩进,FirstLineIndent为每段首行相对左缩进的偏移,单位为毫米
|
||||||
// 零值保持显式换行和左对齐
|
// AutoBidi自动识别双向文字并启用塑形,零值保持显式换行和左对齐
|
||||||
type TextLayout struct {
|
type TextLayout struct {
|
||||||
|
AutoBidi bool
|
||||||
Shape bool
|
Shape bool
|
||||||
Shaping TextShapeOptions
|
Shaping TextShapeOptions
|
||||||
Wrap bool
|
Wrap bool
|
||||||
@@ -256,7 +257,7 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
}
|
}
|
||||||
check = *obj
|
check = *obj
|
||||||
check.TextCode = codes
|
check.TextCode = codes
|
||||||
position := 0
|
position, mapping := 0, 0
|
||||||
for i, code := range codes {
|
for i, code := range codes {
|
||||||
before, after := textCodeRunes(obj.TextCode[i].Value), textCodeRunes(code.Value)
|
before, after := textCodeRunes(obj.TextCode[i].Value), textCodeRunes(code.Value)
|
||||||
if len(before) == len(after) {
|
if len(before) == len(after) {
|
||||||
@@ -264,11 +265,11 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
if before[j] == char {
|
if before[j] == char {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for k := range transforms {
|
for mapping < len(transforms) && position+j >= transforms[mapping].CodePosition+transforms[mapping].CodeCount {
|
||||||
transform := &transforms[k]
|
mapping++
|
||||||
if position+j < transform.CodePosition || position+j >= transform.CodePosition+transform.CodeCount {
|
}
|
||||||
continue
|
if mapping < len(transforms) && position+j >= transforms[mapping].CodePosition {
|
||||||
}
|
transform := &transforms[mapping]
|
||||||
if transform.CodeCount != 1 || transform.GlyphCount != 1 {
|
if transform.CodeCount != 1 || transform.GlyphCount != 1 {
|
||||||
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing a combined glyph requires explicit layout")}
|
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing a combined glyph requires explicit layout")}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,22 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Actions 返回对象动作的独立副本,不修改原对象
|
||||||
|
// 返回: []Action 动作列表
|
||||||
|
func (o GraphicObject) Actions() []Action {
|
||||||
|
switch o.Type {
|
||||||
|
case "TextObject":
|
||||||
|
return cloneEditorData(o.TextObject.Actions)
|
||||||
|
case "PathObject", "Path":
|
||||||
|
return cloneEditorData(o.PathObject.Actions)
|
||||||
|
case "ImageObject":
|
||||||
|
return cloneEditorData(o.ImageObject.Actions)
|
||||||
|
case "CompositeObject", "CompositeGraphicUnit":
|
||||||
|
return cloneEditorData(o.CompositeGraphicUnit.Actions)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析路径并区分省略的虚线样式与显式实线
|
// UnmarshalXML 解析路径并区分省略的虚线样式与显式实线
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ type Renderer struct {
|
|||||||
// RendererOption 渲染器配置选项
|
// RendererOption 渲染器配置选项
|
||||||
type RendererOption func(*Renderer)
|
type RendererOption func(*Renderer)
|
||||||
|
|
||||||
|
// ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置
|
||||||
|
func (r *Renderer) ClearCache() {
|
||||||
|
r.backendStates = make(map[any]any)
|
||||||
|
r.imageCache = make(map[string]image.Image)
|
||||||
|
r.templatePageCache = make(map[string]*PageContent)
|
||||||
|
r.resetFontCache()
|
||||||
|
}
|
||||||
|
|
||||||
// SetFontFS 替换外部字体文件系统并重置字体缓存
|
// SetFontFS 替换外部字体文件系统并重置字体缓存
|
||||||
// 入参: fsys 字体文件系统
|
// 入参: fsys 字体文件系统
|
||||||
func (r *Renderer) SetFontFS(fsys ...fs.FS) {
|
func (r *Renderer) SetFontFS(fsys ...fs.FS) {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/unicode/bidi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LayoutText 按自有字体度量重排横向文字,保留绘制属性,可显式启用字体塑形
|
// LayoutText 按自有字体度量重排横向文字,保留绘制属性,可显式启用字体塑形
|
||||||
@@ -76,6 +78,18 @@ func LayoutText(obj *TextObject, value string, options TextLayout, metrics FontM
|
|||||||
if lineHeight == 0 {
|
if lineHeight == 0 {
|
||||||
lineHeight = math.Max(obj.Size, float64(int(ascender)+int(descender)+int(gap))*unit)
|
lineHeight = math.Max(obj.Size, float64(int(ascender)+int(descender)+int(gap))*unit)
|
||||||
}
|
}
|
||||||
|
if options.AutoBidi && !options.Shaping.Bidi {
|
||||||
|
for _, char := range value {
|
||||||
|
properties, _ := bidi.LookupRune(char)
|
||||||
|
switch properties.Class() {
|
||||||
|
case bidi.R, bidi.AL, bidi.RLE, bidi.RLO, bidi.RLI, bidi.FSI:
|
||||||
|
options.Shaping.Bidi = true
|
||||||
|
}
|
||||||
|
if options.Shaping.Bidi {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if options.Shape || options.Shaping != (TextShapeOptions{}) {
|
if options.Shape || options.Shaping != (TextShapeOptions{}) {
|
||||||
return layoutShapedText(obj, value, options, metrics, width, lineHeight, hScale, float64(ascender)*unit)
|
return layoutShapedText(obj, value, options, metrics, width, lineHeight, hScale, float64(ascender)*unit)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-8
@@ -293,20 +293,18 @@ func validateTextGlyphs(obj TextObject, glyphCount uint16) error {
|
|||||||
total += len(textCodeRunes(code.Value))
|
total += len(textCodeRunes(code.Value))
|
||||||
ends = append(ends, total)
|
ends = append(ends, total)
|
||||||
}
|
}
|
||||||
previous := 0
|
previous, boundary := 0, 0
|
||||||
for _, transform := range obj.CGTransform {
|
for _, transform := range obj.CGTransform {
|
||||||
start, count := transform.CodePosition, transform.CodeCount
|
start, count := transform.CodePosition, transform.CodeCount
|
||||||
if start < previous || count <= 0 || start >= total || count > total-start {
|
if start < previous || count <= 0 || start >= total || count > total-start {
|
||||||
return fmt.Errorf("invalid glyph character range")
|
return fmt.Errorf("invalid glyph character range")
|
||||||
}
|
}
|
||||||
end := start + count
|
end := start + count
|
||||||
for _, boundary := range ends {
|
for ends[boundary] <= start {
|
||||||
if start < boundary {
|
boundary++
|
||||||
if end > boundary {
|
}
|
||||||
return fmt.Errorf("glyph mapping crosses text codes")
|
if end > ends[boundary] {
|
||||||
}
|
return fmt.Errorf("glyph mapping crosses text codes")
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ids := strings.Fields(transform.Glyphs)
|
ids := strings.Fields(transform.Glyphs)
|
||||||
if len(ids) == 0 || len(ids) != transform.GlyphCount {
|
if len(ids) == 0 || len(ids) != transform.GlyphCount {
|
||||||
|
|||||||
Reference in new issue
Block a user