mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
4487fbd55a
commit
13eced8dd8
11 files changed
+422
-98
No files matched your search
@@ -166,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<dialog id="batchPagesPanel" class="form-dialog" aria-label="批量页面">
|
<dialog id="batchPagesPanel" class="form-dialog" aria-label="批量页面">
|
||||||
<form id="batchPagesForm">
|
<form id="batchPagesForm">
|
||||||
<div class="form-row"><label for="batchPageAction">操作</label><select id="batchPageAction"><option value="copy">复制</option><option value="move">移动</option><option value="delete">删除</option></select></div>
|
<div class="form-row"><label for="batchPageAction">操作</label><select id="batchPageAction"><option value="copy">复制</option><option value="move">移动</option><option value="delete">删除</option><option value="save">另存</option></select></div>
|
||||||
<div class="form-row"><label for="batchPageRange">页码</label><input id="batchPageRange" type="text" required autocomplete="off" spellcheck="false" autofocus></div>
|
<div class="form-row"><label for="batchPageRange">页码</label><input id="batchPageRange" type="text" required autocomplete="off" spellcheck="false" autofocus></div>
|
||||||
<div id="batchPagePositionRow" class="form-row" hidden><label for="batchPagePosition">位置</label><select id="batchPagePosition"><option value="before">页前</option><option value="after">页后</option><option value="first">首部</option><option value="last">末尾</option></select></div>
|
<div id="batchPagePositionRow" class="form-row" hidden><label for="batchPagePosition">位置</label><select id="batchPagePosition"><option value="before">页前</option><option value="after">页后</option><option value="first">首部</option><option value="last">末尾</option></select></div>
|
||||||
<div class="form-actions"><div id="batchPagesStatus" class="form-status" role="status"></div><button id="batchPagesCancel" class="button" type="button">取消</button><button id="batchPagesSubmit" class="button" type="submit">复制</button></div>
|
<div class="form-actions"><div id="batchPagesStatus" class="form-status" role="status"></div><button id="batchPagesCancel" class="button" type="button">取消</button><button id="batchPagesSubmit" class="button" type="submit">复制</button></div>
|
||||||
@@ -257,8 +257,8 @@
|
|||||||
<dialog id="infoPanel" class="form-dialog" aria-label="文档信息">
|
<dialog id="infoPanel" class="form-dialog" aria-label="文档信息">
|
||||||
<form id="infoForm">
|
<form id="infoForm">
|
||||||
<label class="form-row"><span>标题</span><input id="infoTitle" type="text" autofocus></label>
|
<label class="form-row"><span>标题</span><input id="infoTitle" type="text" autofocus></label>
|
||||||
<label class="form-row"><span>作者</span><input id="infoAuthor" type="text"></label>
|
|
||||||
<label class="form-row"><span>主题</span><input id="infoSubject" type="text"></label>
|
<label class="form-row"><span>主题</span><input id="infoSubject" type="text"></label>
|
||||||
|
<label class="form-row"><span>作者</span><input id="infoAuthor" type="text"></label>
|
||||||
<div class="form-actions"><div id="infoStatus" class="form-status" role="status"></div><button id="infoCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
<div class="form-actions"><div id="infoStatus" class="form-status" role="status"></div><button id="infoCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -1155,11 +1155,17 @@ button.outline-link:hover {
|
|||||||
|
|
||||||
.page-list-item[aria-current] {
|
.page-list-item[aria-current] {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
background: #e4f3ef;
|
|
||||||
color: var(--accent-strong);
|
color: var(--accent-strong);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-list-item[aria-current]:not([aria-pressed="false"]),
|
||||||
|
.page-list-item[aria-pressed="true"] {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: #e4f3ef;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
.thumb-paper {
|
.thumb-paper {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
|||||||
+196
-31
@@ -39,6 +39,9 @@ const state = {
|
|||||||
savedRevision: null,
|
savedRevision: null,
|
||||||
insertObject: null,
|
insertObject: null,
|
||||||
importPageCount: 0,
|
importPageCount: 0,
|
||||||
|
importing: false,
|
||||||
|
pageSelection: new Set(),
|
||||||
|
pageSelectionAnchor: null,
|
||||||
outlineSelection: null,
|
outlineSelection: null,
|
||||||
outlineAction: "add",
|
outlineAction: "add",
|
||||||
styleOriginal: null,
|
styleOriginal: null,
|
||||||
@@ -571,7 +574,10 @@ el.addPageButton.addEventListener("change", async () => {
|
|||||||
const page = currentPageInfo();
|
const page = currentPageInfo();
|
||||||
return changeDocument("ofdgoChangePage", null, "add", state.pageIndex, page.width, page.height);
|
return changeDocument("ofdgoChangePage", null, "add", state.pageIndex, page.width, page.height);
|
||||||
});
|
});
|
||||||
el.importCancel.addEventListener("click", () => el.importPanel.close());
|
el.importCancel.addEventListener("click", () => state.importing ? cancelExport() : el.importPanel.close());
|
||||||
|
el.importPanel.addEventListener("cancel", event => {
|
||||||
|
if (state.importing) { event.preventDefault(); cancelExport(); }
|
||||||
|
});
|
||||||
el.importPanel.addEventListener("close", () => {
|
el.importPanel.addEventListener("close", () => {
|
||||||
state.importPageCount = 0;
|
state.importPageCount = 0;
|
||||||
el.importForm.reset();
|
el.importForm.reset();
|
||||||
@@ -592,29 +598,41 @@ el.importForm.addEventListener("submit", async event => {
|
|||||||
const saved = await changeDocument("ofdgoImportPages", null, el.importRange.value === "custom" ? el.importPages.value : "", position, el.importOutlines.checked);
|
const saved = await changeDocument("ofdgoImportPages", null, el.importRange.value === "custom" ? el.importPages.value : "", position, el.importOutlines.checked);
|
||||||
if (saved) el.importPanel.close();
|
if (saved) el.importPanel.close();
|
||||||
});
|
});
|
||||||
editorClick(el.copyPageButton, () => changeDocument("ofdgoChangePage", null, "copy", state.pageIndex));
|
editorClick(el.copyPageButton, () => changeSelectedPages("copy"));
|
||||||
editorClick(el.deletePageButton, () => changeDocument("ofdgoChangePage", null, "delete", state.pageIndex));
|
editorClick(el.deletePageButton, () => changeSelectedPages("delete"));
|
||||||
editorClick(el.movePagePrevButton, () => changeDocument("ofdgoChangePage", null, "move", state.pageIndex, state.pageIndex - 1));
|
editorClick(el.movePagePrevButton, () => changeSelectedPages("move", -1));
|
||||||
editorClick(el.movePageNextButton, () => changeDocument("ofdgoChangePage", null, "move", state.pageIndex, state.pageIndex + 1));
|
editorClick(el.movePageNextButton, () => changeSelectedPages("move", 1));
|
||||||
editorClick(el.pageSettingsButton, openPagePanel);
|
editorClick(el.pageSettingsButton, openPagePanel);
|
||||||
editorClick(el.batchPagesButton, () => {
|
editorClick(el.batchPagesButton, openBatchPages);
|
||||||
|
function openBatchPages() {
|
||||||
el.batchPageAction.value = "copy";
|
el.batchPageAction.value = "copy";
|
||||||
el.batchPagesSubmit.textContent = "复制";
|
el.batchPagesSubmit.textContent = "复制";
|
||||||
el.batchPagePositionRow.hidden = true;
|
el.batchPagePositionRow.hidden = true;
|
||||||
el.batchPagePosition.value = "after";
|
el.batchPagePosition.value = "after";
|
||||||
el.batchPageRange.value = String(state.pageIndex + 1);
|
el.batchPageRange.value = selectedPageRange();
|
||||||
el.batchPagesStatus.textContent = "";
|
el.batchPagesStatus.textContent = "";
|
||||||
el.batchPagesPanel.showModal();
|
el.batchPagesPanel.showModal();
|
||||||
el.batchPageRange.select();
|
el.batchPageRange.select();
|
||||||
});
|
}
|
||||||
el.batchPagesCancel.addEventListener("click", () => el.batchPagesPanel.close());
|
el.batchPagesCancel.addEventListener("click", () => el.batchPagesPanel.close());
|
||||||
el.batchPageAction.addEventListener("change", () => {
|
el.batchPageAction.addEventListener("change", () => {
|
||||||
el.batchPagesSubmit.textContent = { copy: "复制", move: "移动", delete: "删除" }[el.batchPageAction.value];
|
el.batchPagesSubmit.textContent = { copy: "复制", move: "移动", delete: "删除", save: "另存" }[el.batchPageAction.value];
|
||||||
el.batchPagePositionRow.hidden = el.batchPageAction.value !== "move";
|
el.batchPagePositionRow.hidden = el.batchPageAction.value !== "move";
|
||||||
});
|
});
|
||||||
el.batchPagesForm.addEventListener("submit", async event => {
|
el.batchPagesForm.addEventListener("submit", async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (document.body.hasAttribute("aria-busy") || !el.batchPagesForm.reportValidity()) return;
|
||||||
const args = [el.batchPageAction.value, el.batchPageRange.value];
|
const args = [el.batchPageAction.value, el.batchPageRange.value];
|
||||||
|
if (args[0] === "save") {
|
||||||
|
const seq = state.openSeq;
|
||||||
|
try {
|
||||||
|
const indices = await callWASM("ofdgoParsePageRange", args[1]);
|
||||||
|
if (seq === state.openSeq && el.batchPagesPanel.open && await exportFile(true, indices, "ofd")) el.batchPagesPanel.close();
|
||||||
|
} catch (err) {
|
||||||
|
if (seq === state.openSeq) el.batchPagesStatus.textContent = err.message;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (args[0] === "move") args.push({ before: state.pageIndex, after: state.pageIndex + 1, first: 0, last: state.doc.pageCount }[el.batchPagePosition.value]);
|
if (args[0] === "move") args.push({ before: state.pageIndex, after: state.pageIndex + 1, first: 0, last: state.doc.pageCount }[el.batchPagePosition.value]);
|
||||||
if (await changeDocument("ofdgoBatchPages", null, ...args)) el.batchPagesPanel.close();
|
if (await changeDocument("ofdgoBatchPages", null, ...args)) el.batchPagesPanel.close();
|
||||||
});
|
});
|
||||||
@@ -1721,7 +1739,9 @@ async function changeDocument(name, item, ...args) {
|
|||||||
const toolbarFocus = el.editorTools.contains(active) ? active : null;
|
const toolbarFocus = el.editorTools.contains(active) ? active : null;
|
||||||
const previous = currentPageInfo();
|
const previous = currentPageInfo();
|
||||||
const { scrollLeft, scrollTop } = el.viewerPanel;
|
const { scrollLeft, scrollTop } = el.viewerPanel;
|
||||||
setBusy(true);
|
const importing = name === "ofdgoImportPages";
|
||||||
|
if (importing) state.importing = true;
|
||||||
|
setBusy(true, importing ? "正在导入" : "", null);
|
||||||
try {
|
try {
|
||||||
args = await Promise.all(args);
|
args = await Promise.all(args);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
@@ -1800,6 +1820,12 @@ async function changeDocument(name, item, ...args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (importing) {
|
||||||
|
state.importing = false;
|
||||||
|
state.exportRequestID = 0;
|
||||||
|
el.cancelExportButton.hidden = true;
|
||||||
|
el.importCancel.disabled = false;
|
||||||
|
}
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
if (toolbarFocus?.isConnected && !toolbarFocus.disabled) toolbarFocus.focus({ preventScroll: true });
|
if (toolbarFocus?.isConnected && !toolbarFocus.disabled) toolbarFocus.focus({ preventScroll: true });
|
||||||
@@ -2196,7 +2222,7 @@ async function loadWASM() {
|
|||||||
setProgress("正在保存", null);
|
setProgress("正在保存", null);
|
||||||
} else if (!el.cancelExportButton.disabled) {
|
} else if (!el.cancelExportButton.disabled) {
|
||||||
if (data.stage === "prepare") {
|
if (data.stage === "prepare") {
|
||||||
const label = { fonts: "正在处理字体", pages: "正在处理页面", references: "正在检查引用", resources: "正在整理资源", write: "正在写入" }[data.phase];
|
const label = { snapshot: "正在准备", ids: "正在检查", commit: "正在整理", fonts: "正在处理字体", pages: "正在处理页面", references: "正在检查引用", resources: "正在整理资源", write: "正在写入" }[data.phase];
|
||||||
setProgress(label, data.total ? data.completed / data.total * 100 : null);
|
setProgress(label, data.total ? data.completed / data.total * 100 : null);
|
||||||
} else if (data.completed === data.total) {
|
} else if (data.completed === data.total) {
|
||||||
setProgress("正在收尾", null);
|
setProgress("正在收尾", null);
|
||||||
@@ -2205,6 +2231,13 @@ async function loadWASM() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (data.type === "import") {
|
||||||
|
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.importing && !el.importCancel.disabled) {
|
||||||
|
const label = { ids: "正在检查", pages: "正在导入", resources: "正在读取", commit: "正在完成" }[data.phase];
|
||||||
|
el.importStatus.textContent = data.total ? `${label}${data.completed}/${data.total}页` : label;
|
||||||
|
setProgress(label, data.total ? data.completed / data.total * 100 : null);
|
||||||
|
if (data.phase === "commit") el.importCancel.disabled = el.cancelExportButton.disabled = true;
|
||||||
|
}
|
||||||
} else if (data.type === "ready") {
|
} else if (data.type === "ready") {
|
||||||
state.ready = true;
|
state.ready = true;
|
||||||
resolve();
|
resolve();
|
||||||
@@ -2305,6 +2338,8 @@ async function openOFD(file) {
|
|||||||
state.ofdBytes = bytes;
|
state.ofdBytes = bytes;
|
||||||
state.fileName = file.name || "ofdgo.ofd";
|
state.fileName = file.name || "ofdgo.ofd";
|
||||||
state.editing = false;
|
state.editing = false;
|
||||||
|
state.pageSelection.clear();
|
||||||
|
state.pageSelectionAnchor = null;
|
||||||
state.objectClipboard = null;
|
state.objectClipboard = null;
|
||||||
state.styleClipboard = null;
|
state.styleClipboard = null;
|
||||||
state.editorInfo = null;
|
state.editorInfo = null;
|
||||||
@@ -2737,6 +2772,10 @@ async function openDocument(options = {}) {
|
|||||||
const pageCount = doc.pageCount || 0;
|
const pageCount = doc.pageCount || 0;
|
||||||
const pageIndex = Math.min(Math.max(options.pageIndex || 0, 0), Math.max(pageCount - 1, 0));
|
const pageIndex = Math.min(Math.max(options.pageIndex || 0, 0), Math.max(pageCount - 1, 0));
|
||||||
state.doc = doc;
|
state.doc = doc;
|
||||||
|
if (options.resetScroll || !options.skipAutoFonts) {
|
||||||
|
state.pageSelection.clear();
|
||||||
|
state.pageSelectionAnchor = null;
|
||||||
|
}
|
||||||
state.pageIndex = pageIndex;
|
state.pageIndex = pageIndex;
|
||||||
state.scale = options.scale || 1;
|
state.scale = options.scale || 1;
|
||||||
if (!options.fitMode) {
|
if (!options.fitMode) {
|
||||||
@@ -2989,7 +3028,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
const extension = archive ? "zip" : format.extension;
|
const extension = archive ? "zip" : format.extension;
|
||||||
const mime = archive ? "application/zip" : format.mime;
|
const mime = archive ? "application/zip" : format.mime;
|
||||||
let openSeq = state.openSeq;
|
let openSeq = state.openSeq;
|
||||||
const fileName = whole ? `${baseFileName()}.${extension}` : pageFileName(extension);
|
const fileName = saving && indices !== null ? `${baseFileName()}_选页.ofd` : whole ? `${baseFileName()}.${extension}` : pageFileName(extension);
|
||||||
const pageIndex = state.pageIndex;
|
const pageIndex = state.pageIndex;
|
||||||
const dpi = exportFormatUsesDPI(format.value) ? currentImageDPI() : 0;
|
const dpi = exportFormatUsesDPI(format.value) ? currentImageDPI() : 0;
|
||||||
state.exporting = true;
|
state.exporting = true;
|
||||||
@@ -3011,7 +3050,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
openSeq = state.openSeq;
|
openSeq = state.openSeq;
|
||||||
setBusy(true, `正在生成 ${label}`, null, whole ? STATUS.exporting : STATUS.pageExporting);
|
setBusy(true, `正在生成 ${label}`, null, whole ? STATUS.exporting : STATUS.pageExporting);
|
||||||
}
|
}
|
||||||
const result = saving ? await callWASM("ofdgoSaveDocument", file) : whole
|
const result = saving ? await callWASM("ofdgoSaveDocument", indices, file) : whole
|
||||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, file)
|
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, file)
|
||||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, file);
|
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, file);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
@@ -3020,13 +3059,15 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
if (result.blob) {
|
if (result.blob) {
|
||||||
downloadBytes(result.blob, result.mime, fileName);
|
downloadBytes(result.blob, result.mime, fileName);
|
||||||
}
|
}
|
||||||
if (saving) {
|
if (saving && indices === null) {
|
||||||
state.savedRevision = state.editorInfo.revision;
|
state.savedRevision = state.editorInfo.revision;
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
}
|
}
|
||||||
setStatus(`${result.label} ${saving ? "保存" : "导出"}完成 ${formatBytes(result.size)}`);
|
setStatus(`${result.label} ${saving ? "保存" : "导出"}完成 ${formatBytes(result.size)}`);
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
|
if (el.batchPagesPanel.open) el.batchPagesStatus.textContent = err.name === "AbortError" ? "导出已取消" : err.message;
|
||||||
if (err.name === "AbortError") {
|
if (err.name === "AbortError") {
|
||||||
setStatus("导出已取消");
|
setStatus("导出已取消");
|
||||||
} else {
|
} else {
|
||||||
@@ -3049,6 +3090,10 @@ function cancelExport() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.cancelExportButton.disabled = true;
|
el.cancelExportButton.disabled = true;
|
||||||
|
if (state.importing) {
|
||||||
|
el.importCancel.disabled = true;
|
||||||
|
el.importStatus.textContent = "正在取消";
|
||||||
|
}
|
||||||
setProgress("正在取消", null);
|
setProgress("正在取消", null);
|
||||||
wasmWorker.postMessage({ type: "cancel", id: state.exportRequestID });
|
wasmWorker.postMessage({ type: "cancel", id: state.exportRequestID });
|
||||||
}
|
}
|
||||||
@@ -3371,22 +3416,24 @@ function mountPageSVG(index, page, openSeq = state.openSeq) {
|
|||||||
canvasEditor.mount(index, page, surface);
|
canvasEditor.mount(index, page, surface);
|
||||||
}
|
}
|
||||||
for (const link of page.links) {
|
for (const link of page.links) {
|
||||||
let url;
|
|
||||||
try {
|
|
||||||
url = new URL(link.uri);
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
anchor.className = "page-link";
|
anchor.className = "page-link";
|
||||||
|
if (link.dest) {
|
||||||
|
const target = state.doc.pages.findIndex(page => page.id === link.dest.pageID);
|
||||||
|
if (target < 0 || !["XYZ", "Fit", "FitH", "FitV", "FitR"].includes(link.dest.type)) continue;
|
||||||
|
anchor.href = `#page-${target + 1}`;
|
||||||
|
anchor.title = `第${target + 1}页`;
|
||||||
|
anchor.addEventListener("click", event => { event.preventDefault(); navigateDestination(link.dest); });
|
||||||
|
} else {
|
||||||
|
let url;
|
||||||
|
try { url = new URL(link.uri); } catch { continue; }
|
||||||
|
if (url.protocol !== "http:" && url.protocol !== "https:") continue;
|
||||||
anchor.href = url.href;
|
anchor.href = url.href;
|
||||||
anchor.target = "_blank";
|
anchor.target = "_blank";
|
||||||
anchor.rel = "noopener noreferrer";
|
anchor.rel = "noopener noreferrer";
|
||||||
anchor.title = url.href;
|
anchor.title = url.href;
|
||||||
anchor.setAttribute("aria-label", url.href);
|
}
|
||||||
|
anchor.setAttribute("aria-label", anchor.title);
|
||||||
anchor.style.left = `${link.x / page.width * 100}%`;
|
anchor.style.left = `${link.x / page.width * 100}%`;
|
||||||
anchor.style.top = `${link.y / page.height * 100}%`;
|
anchor.style.top = `${link.y / page.height * 100}%`;
|
||||||
anchor.style.width = `${link.width / page.width * 100}%`;
|
anchor.style.width = `${link.width / page.width * 100}%`;
|
||||||
@@ -3400,6 +3447,51 @@ function mountPageSVG(index, page, openSeq = state.openSeq) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function destinationPoint(page, x, y) {
|
||||||
|
if (state.rotation === 90) return { x: page.height - y, y: x };
|
||||||
|
if (state.rotation === 180) return { x: page.width - x, y: page.height - y };
|
||||||
|
if (state.rotation === 270) return { x: y, y: page.width - x };
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigateDestination(dest) {
|
||||||
|
if (document.body.hasAttribute("aria-busy")) return;
|
||||||
|
const index = state.doc.pages.findIndex(page => page.id === dest.pageID);
|
||||||
|
if (index < 0) return;
|
||||||
|
const seq = state.openSeq, scale = state.scale;
|
||||||
|
await renderPage(index, { fit: false, scroll: false });
|
||||||
|
if (seq !== state.openSeq || state.pageIndex !== index) return;
|
||||||
|
const page = state.doc.pages[index], size = pageViewSize(page), space = pageSpace();
|
||||||
|
const width = el.viewerPanel.clientWidth - space * 2, height = el.viewerPanel.clientHeight - space * 2;
|
||||||
|
let left = dest.left, top = dest.top;
|
||||||
|
if (dest.type === "Fit") {
|
||||||
|
fitHeight(false);
|
||||||
|
scrollToPage(index);
|
||||||
|
return;
|
||||||
|
} else if (dest.type === "FitH") {
|
||||||
|
setScale(fitWidthScale(page), false);
|
||||||
|
left = 0;
|
||||||
|
} else if (dest.type === "FitV") {
|
||||||
|
setScale(height / (size.height * MM_TO_PX), false);
|
||||||
|
top = 0;
|
||||||
|
} else if (dest.type === "FitR") {
|
||||||
|
const a = destinationPoint(page, dest.left, dest.top), b = destinationPoint(page, dest.right, dest.bottom);
|
||||||
|
if (dest.right <= dest.left || dest.bottom <= dest.top) return;
|
||||||
|
setScale(Math.min(width / Math.abs(b.x - a.x), height / Math.abs(b.y - a.y)) / MM_TO_PX, false);
|
||||||
|
} else {
|
||||||
|
setScale(dest.zoom > 0 ? dest.zoom : scale, false);
|
||||||
|
}
|
||||||
|
const point = destinationPoint(page, left, top);
|
||||||
|
if (dest.type === "FitR") {
|
||||||
|
const bottom = destinationPoint(page, dest.right, dest.bottom);
|
||||||
|
point.x = Math.min(point.x, bottom.x);
|
||||||
|
point.y = Math.min(point.y, bottom.y);
|
||||||
|
}
|
||||||
|
const shell = pageShell(index).getBoundingClientRect(), viewer = el.viewerPanel.getBoundingClientRect();
|
||||||
|
el.viewerPanel.scrollLeft += shell.left - viewer.left + point.x * MM_TO_PX * state.scale - space;
|
||||||
|
el.viewerPanel.scrollTop += shell.top - viewer.top + point.y * MM_TO_PX * state.scale - pageBlockSpace();
|
||||||
|
}
|
||||||
|
|
||||||
function createTextLayer(text) {
|
function createTextLayer(text) {
|
||||||
const layer = document.createElement("div");
|
const layer = document.createElement("div");
|
||||||
layer.className = "text-layer";
|
layer.className = "text-layer";
|
||||||
@@ -4168,6 +4260,71 @@ function createOutlineList(outlines, path = []) {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectedPageIndexes() {
|
||||||
|
if (!state.pageSelection.size) return [state.pageIndex];
|
||||||
|
return state.doc.pages.filter(page => state.pageSelection.has(page.id)).map(page => page.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSelectedPages(action, direction = 0) {
|
||||||
|
const indexes = selectedPageIndexes();
|
||||||
|
if (indexes.length === 1) return changeDocument("ofdgoChangePage", null, action, indexes[0], ...(direction ? [indexes[0] + direction] : []));
|
||||||
|
const at = direction < 0 ? indexes[0] - 1 : indexes.at(-1) + 2;
|
||||||
|
return changeDocument("ofdgoBatchPages", null, action, selectedPageRange(), ...(direction ? [at] : []));
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedPageRange() {
|
||||||
|
const pages = selectedPageIndexes().map(index => index + 1);
|
||||||
|
const ranges = [];
|
||||||
|
for (let i = 0; i < pages.length; i++) {
|
||||||
|
const first = pages[i];
|
||||||
|
while (pages[i + 1] === pages[i] + 1) i++;
|
||||||
|
ranges.push(first === pages[i] ? String(first) : `${first}-${pages[i]}`);
|
||||||
|
}
|
||||||
|
return ranges.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncPageSelection() {
|
||||||
|
for (const button of el.pageList.children) {
|
||||||
|
if (state.editing) button.setAttribute("aria-pressed", String(state.pageSelection.has(state.doc.pages[Number(button.dataset.pageIndex)].id)));
|
||||||
|
else button.removeAttribute("aria-pressed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectThumbnailPage(event, index) {
|
||||||
|
if (document.body.hasAttribute("aria-busy")) return;
|
||||||
|
if (state.editing) {
|
||||||
|
const page = state.doc.pages[index], additive = event.ctrlKey || event.metaKey;
|
||||||
|
if (!additive) state.pageSelection.clear();
|
||||||
|
if (event.shiftKey) {
|
||||||
|
let anchor = state.doc.pages.findIndex(page => page.id === state.pageSelectionAnchor);
|
||||||
|
if (anchor < 0) anchor = state.pageIndex;
|
||||||
|
for (let i = Math.min(anchor, index); i <= Math.max(anchor, index); i++) state.pageSelection.add(state.doc.pages[i].id);
|
||||||
|
} else {
|
||||||
|
if (additive && state.pageSelection.has(page.id)) state.pageSelection.delete(page.id);
|
||||||
|
else state.pageSelection.add(page.id);
|
||||||
|
state.pageSelectionAnchor = page.id;
|
||||||
|
}
|
||||||
|
syncPageSelection();
|
||||||
|
updateControls();
|
||||||
|
if (additive || event.shiftKey) return;
|
||||||
|
}
|
||||||
|
return renderPage(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.pageList.addEventListener("keydown", event => {
|
||||||
|
if (!state.editing || event.isComposing || document.body.hasAttribute("aria-busy")) return;
|
||||||
|
if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "a") {
|
||||||
|
state.pageSelection = new Set(state.doc.pages.map(page => page.id));
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
state.pageSelection.clear();
|
||||||
|
state.pageSelectionAnchor = null;
|
||||||
|
} else return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
syncPageSelection();
|
||||||
|
updateControls();
|
||||||
|
});
|
||||||
|
|
||||||
function renderPageList() {
|
function renderPageList() {
|
||||||
el.pageList.replaceChildren();
|
el.pageList.replaceChildren();
|
||||||
if (!state.doc) {
|
if (!state.doc) {
|
||||||
@@ -4176,6 +4333,8 @@ function renderPageList() {
|
|||||||
if (state.thumbnailObserver) {
|
if (state.thumbnailObserver) {
|
||||||
state.thumbnailObserver.disconnect();
|
state.thumbnailObserver.disconnect();
|
||||||
}
|
}
|
||||||
|
const ids = new Set(state.doc.pages.map(page => page.id));
|
||||||
|
for (const id of state.pageSelection) if (!ids.has(id) || !state.editing) state.pageSelection.delete(id);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
const thumbnailTargets = [];
|
const thumbnailTargets = [];
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
@@ -4203,11 +4362,12 @@ function renderPageList() {
|
|||||||
|
|
||||||
button.append(thumb, label, size);
|
button.append(thumb, label, size);
|
||||||
if (state.editing && pageCan("move", page.index)) enablePageDrag(button, page.index);
|
if (state.editing && pageCan("move", page.index)) enablePageDrag(button, page.index);
|
||||||
button.addEventListener("click", () => renderPage(page.index));
|
button.addEventListener("click", event => selectThumbnailPage(event, page.index));
|
||||||
fragment.append(button);
|
fragment.append(button);
|
||||||
thumbnailTargets.push([button, page.index]);
|
thumbnailTargets.push([button, page.index]);
|
||||||
}
|
}
|
||||||
el.pageList.append(fragment);
|
el.pageList.append(fragment);
|
||||||
|
syncPageSelection();
|
||||||
for (const [button, index] of thumbnailTargets) {
|
for (const [button, index] of thumbnailTargets) {
|
||||||
observeThumbnail(button, index, openSeq);
|
observeThumbnail(button, index, openSeq);
|
||||||
}
|
}
|
||||||
@@ -5170,11 +5330,13 @@ function updateEditorTools() {
|
|||||||
el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert");
|
el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert");
|
||||||
el.saveButton.disabled = el.addPageButton.disabled = pagesDisabled;
|
el.saveButton.disabled = el.addPageButton.disabled = pagesDisabled;
|
||||||
el.batchPagesButton.disabled = pagesDisabled;
|
el.batchPagesButton.disabled = pagesDisabled;
|
||||||
el.copyPageButton.disabled = pagesDisabled || !pageCan("copy");
|
const selectedPages = state.editing ? selectedPageIndexes() : [state.pageIndex];
|
||||||
|
el.copyPageButton.disabled = pagesDisabled || selectedPages.some(index => !pageCan("copy", index));
|
||||||
el.pageSettingsButton.disabled = pagesDisabled || !pageCan("resize");
|
el.pageSettingsButton.disabled = pagesDisabled || !pageCan("resize");
|
||||||
el.deletePageButton.disabled = pagesDisabled || !pageCan("delete") || state.doc?.pageCount <= 1;
|
el.deletePageButton.disabled = pagesDisabled || selectedPages.some(index => !pageCan("delete", index)) || selectedPages.length >= state.doc?.pageCount;
|
||||||
el.movePagePrevButton.disabled = pagesDisabled || !pageCan("move") || state.pageIndex === 0;
|
const moveDisabled = pagesDisabled || selectedPages.some(index => !pageCan("move", index));
|
||||||
el.movePageNextButton.disabled = pagesDisabled || !pageCan("move") || state.pageIndex === state.doc?.pageCount - 1;
|
el.movePagePrevButton.disabled = moveDisabled || selectedPages[0] === 0;
|
||||||
|
el.movePageNextButton.disabled = moveDisabled || selectedPages.at(-1) === state.doc?.pageCount - 1;
|
||||||
const enabled = state.editing && state.selectObjects && !state.panMode;
|
const enabled = state.editing && state.selectObjects && !state.panMode;
|
||||||
canvasEditor.setEnabled(enabled);
|
canvasEditor.setEnabled(enabled);
|
||||||
updateObjectControls(canvasEditor.selected);
|
updateObjectControls(canvasEditor.selected);
|
||||||
@@ -5283,7 +5445,7 @@ async function callWASM(name, ...args) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = ++wasmRequestID;
|
const id = ++wasmRequestID;
|
||||||
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
||||||
if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument") {
|
if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoImportPages") {
|
||||||
state.exportRequestID = id;
|
state.exportRequestID = id;
|
||||||
el.cancelExportButton.hidden = false;
|
el.cancelExportButton.hidden = false;
|
||||||
el.cancelExportButton.disabled = false;
|
el.cancelExportButton.disabled = false;
|
||||||
@@ -5318,7 +5480,10 @@ 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.importForm.inert = busy;
|
el.importForm.inert = busy && !state.importing;
|
||||||
|
for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy;
|
||||||
|
el.importPages.disabled = busy || el.importRange.value !== "custom";
|
||||||
|
el.importSubmit.disabled = busy || !state.importPageCount;
|
||||||
el.batchPagesForm.inert = busy;
|
el.batchPagesForm.inert = busy;
|
||||||
el.objectStyleForm.inert = busy;
|
el.objectStyleForm.inert = busy;
|
||||||
el.objectBoundsForm.inert = busy;
|
el.objectBoundsForm.inert = busy;
|
||||||
|
|||||||
+27
-16
@@ -1,36 +1,45 @@
|
|||||||
importScripts("./wasm_exec.js");
|
importScripts("./wasm_exec.js");
|
||||||
|
|
||||||
let pending = Promise.resolve();
|
let pending = Promise.resolve();
|
||||||
const exports = new Map();
|
const operations = new Map();
|
||||||
self.onmessage = ({ data }) => {
|
self.onmessage = ({ data }) => {
|
||||||
if (data.type === "cancel") {
|
if (data.type === "cancel") {
|
||||||
exports.get(data.id)?.abort();
|
operations.get(data.id)?.abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument") {
|
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoImportPages") {
|
||||||
exports.set(data.id, new AbortController());
|
operations.set(data.id, new AbortController());
|
||||||
}
|
}
|
||||||
pending = pending.then(() => handleMessage(data));
|
pending = pending.then(() => handleMessage(data));
|
||||||
};
|
};
|
||||||
async function handleMessage({ id, name, args }) {
|
async function handleMessage({ id, name, args }) {
|
||||||
let output;
|
let output;
|
||||||
let channel;
|
let channel;
|
||||||
const signal = exports.get(id)?.signal;
|
const signal = operations.get(id)?.signal;
|
||||||
|
const importing = name === "ofdgoImportPages";
|
||||||
try {
|
try {
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
let size = 0;
|
let size = 0;
|
||||||
if (signal) {
|
if (signal) {
|
||||||
const file = args.pop();
|
|
||||||
if (file) {
|
|
||||||
output = await file.createWritable();
|
|
||||||
}
|
|
||||||
signal.throwIfAborted();
|
|
||||||
channel = new MessageChannel();
|
channel = new MessageChannel();
|
||||||
const finish = (done, err) => {
|
const finish = (done, err, commit = false) => {
|
||||||
channel.port1.onmessage = () => done(err?.message || "", signal.aborted);
|
channel.port1.onmessage = () => {
|
||||||
|
if (commit && !signal.aborted && !err) operations.delete(id);
|
||||||
|
done(err?.message || "", signal.aborted);
|
||||||
|
};
|
||||||
channel.port2.postMessage(null);
|
channel.port2.postMessage(null);
|
||||||
};
|
};
|
||||||
|
if (importing) {
|
||||||
|
args.push((phase, completed, total, done) => {
|
||||||
|
self.postMessage({ id, type: "import", phase, completed, total });
|
||||||
|
finish(done, null, phase === "commit");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const file = args.pop();
|
||||||
|
const indices = name === "ofdgoSaveDocument" ? args.shift() : null;
|
||||||
|
if (file) output = await file.createWritable();
|
||||||
|
signal.throwIfAborted();
|
||||||
args.push((bytes, done) => {
|
args.push((bytes, done) => {
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
finish(done);
|
finish(done);
|
||||||
@@ -54,13 +63,15 @@ async function handleMessage({ id, name, args }) {
|
|||||||
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||||
finish(done);
|
finish(done);
|
||||||
});
|
});
|
||||||
|
if (indices != null) args.push(indices);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const payload = await globalThis[name](...args);
|
const payload = await globalThis[name](...args);
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
||||||
if (signal && result.ok) {
|
if (signal && !importing && result.ok) {
|
||||||
exports.delete(id);
|
operations.delete(id);
|
||||||
result.data.size = size;
|
result.data.size = size;
|
||||||
self.postMessage({ id, type: "export", stage: "save" });
|
self.postMessage({ id, type: "export", stage: "save" });
|
||||||
if (output) {
|
if (output) {
|
||||||
@@ -78,14 +89,14 @@ async function handleMessage({ id, name, args }) {
|
|||||||
await output?.abort().catch(() => {});
|
await output?.abort().catch(() => {});
|
||||||
const result = { id, ok: false, error: err.message };
|
const result = { id, ok: false, error: err.message };
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
result.error = "导出已取消";
|
result.error = importing ? "导入已取消" : "导出已取消";
|
||||||
result.canceled = true;
|
result.canceled = true;
|
||||||
}
|
}
|
||||||
self.postMessage(result);
|
self.postMessage(result);
|
||||||
} finally {
|
} finally {
|
||||||
channel?.port1.close();
|
channel?.port1.close();
|
||||||
channel?.port2.close();
|
channel?.port2.close();
|
||||||
exports.delete(id);
|
operations.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+42
-21
@@ -104,9 +104,9 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoPageTextString", pageTextString)
|
registerCallback("ofdgoPageTextString", pageTextString)
|
||||||
registerCallback("ofdgoExportFormats", exportFormats)
|
registerCallback("ofdgoExportFormats", exportFormats)
|
||||||
registerCallback("ofdgoParsePageRange", parsePageRange)
|
registerCallback("ofdgoParsePageRange", parsePageRange)
|
||||||
registerExportCallback("ofdgoExportPage", exportPage)
|
registerAsyncCallback("ofdgoExportPage", exportPage)
|
||||||
registerExportCallback("ofdgoExportDocument", exportDocument)
|
registerAsyncCallback("ofdgoExportDocument", exportDocument)
|
||||||
registerExportCallback("ofdgoExportAttachment", exportAttachment)
|
registerAsyncCallback("ofdgoExportAttachment", exportAttachment)
|
||||||
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
||||||
registerCallback("ofdgoFontFaces", fontFaces)
|
registerCallback("ofdgoFontFaces", fontFaces)
|
||||||
registerCallback("ofdgoFontFace", fontFace)
|
registerCallback("ofdgoFontFace", fontFace)
|
||||||
@@ -122,7 +122,7 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoPasteStyle", pasteStyle)
|
registerCallback("ofdgoPasteStyle", pasteStyle)
|
||||||
registerCallback("ofdgoResizeObjects", resizeObjects)
|
registerCallback("ofdgoResizeObjects", resizeObjects)
|
||||||
registerCallback("ofdgoLoadImport", loadImport)
|
registerCallback("ofdgoLoadImport", loadImport)
|
||||||
registerCallback("ofdgoImportPages", importPages)
|
registerAsyncCallback("ofdgoImportPages", importPages)
|
||||||
registerCallback("ofdgoInsertText", insertText)
|
registerCallback("ofdgoInsertText", insertText)
|
||||||
registerCallback("ofdgoUpdateText", updateText)
|
registerCallback("ofdgoUpdateText", updateText)
|
||||||
registerCallback("ofdgoStyleText", styleText)
|
registerCallback("ofdgoStyleText", styleText)
|
||||||
@@ -154,7 +154,7 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoDeleteObject", deleteObject)
|
registerCallback("ofdgoDeleteObject", deleteObject)
|
||||||
registerCallback("ofdgoUndo", func([]js.Value) (any, error) { return restoreEditor(false) })
|
registerCallback("ofdgoUndo", func([]js.Value) (any, error) { return restoreEditor(false) })
|
||||||
registerCallback("ofdgoRedo", func([]js.Value) (any, error) { return restoreEditor(true) })
|
registerCallback("ofdgoRedo", func([]js.Value) (any, error) { return restoreEditor(true) })
|
||||||
registerExportCallback("ofdgoSaveDocument", saveDocument)
|
registerAsyncCallback("ofdgoSaveDocument", saveDocument)
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +167,9 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) {
|
|||||||
js.Global().Set(name, cb)
|
js.Global().Set(name, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerExportCallback 异步导出,为浏览器处理数据块让出事件循环
|
// registerAsyncCallback 注册异步回调,为浏览器处理数据和取消请求让出事件循环
|
||||||
// 入参: name 回调名称, fn 回调函数
|
// 入参: name 回调名称, fn 回调函数
|
||||||
func registerExportCallback(name string, fn func([]js.Value) (any, error)) {
|
func registerAsyncCallback(name string, fn func([]js.Value) (any, error)) {
|
||||||
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
|
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
executor := js.FuncOf(func(this js.Value, callbacks []js.Value) any {
|
executor := js.FuncOf(func(this js.Value, callbacks []js.Value) any {
|
||||||
resolve := callbacks[0]
|
resolve := callbacks[0]
|
||||||
@@ -349,13 +349,18 @@ func renderPage(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
links := make([]any, len(page.Links))
|
links := make([]any, len(page.Links))
|
||||||
for i, link := range page.Links {
|
for i, link := range page.Links {
|
||||||
links[i] = map[string]any{
|
item := map[string]any{
|
||||||
"uri": link.URI,
|
"uri": link.URI,
|
||||||
"x": link.Box.X,
|
"x": link.Box.X,
|
||||||
"y": link.Box.Y,
|
"y": link.Box.Y,
|
||||||
"width": link.Box.W,
|
"width": link.Box.W,
|
||||||
"height": link.Box.H,
|
"height": link.Box.H,
|
||||||
}
|
}
|
||||||
|
if dest := link.Dest; dest != nil {
|
||||||
|
item["dest"] = map[string]any{"type": dest.Type, "pageID": dest.PageID, "left": dest.Left, "top": dest.Top,
|
||||||
|
"right": dest.Right, "bottom": dest.Bottom, "zoom": dest.Zoom}
|
||||||
|
}
|
||||||
|
links[i] = item
|
||||||
}
|
}
|
||||||
fonts := make([]any, len(page.Fonts))
|
fonts := make([]any, len(page.Fonts))
|
||||||
for i, font := range page.Fonts {
|
for i, font := range page.Fonts {
|
||||||
@@ -1283,7 +1288,7 @@ func loadImport(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// importPages 插入指定来源页面并保持目标文档元数据
|
// importPages 插入指定来源页面并保持目标文档元数据
|
||||||
// 入参: args 页码表达式,空值为全部、目标零基插入位置和是否导入对应目录
|
// 入参: args 页码表达式,空值为全部、目标零基插入位置、是否导入对应目录和可选进度回调
|
||||||
// 返回: any 文档信息及首个插入页, error 错误信息
|
// 返回: any 文档信息及首个插入页, error 错误信息
|
||||||
func importPages(args []js.Value) (any, error) {
|
func importPages(args []js.Value) (any, error) {
|
||||||
if currentEditor == nil || pendingImport == nil {
|
if currentEditor == nil || pendingImport == nil {
|
||||||
@@ -1306,6 +1311,9 @@ func importPages(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
at := args[1].Int()
|
at := args[1].Int()
|
||||||
options := ofdgo.PageImportOptions{Outlines: len(args) > 2 && args[2].Bool()}
|
options := ofdgo.PageImportOptions{Outlines: len(args) > 2 && args[2].Bool()}
|
||||||
|
if len(args) > 3 {
|
||||||
|
options.OnProgress = editorOperationProgress(args[3])
|
||||||
|
}
|
||||||
if _, err := currentEditor.ImportPagesWithOptions(pendingImport, indexes, at, options); err != nil {
|
if _, err := currentEditor.ImportPagesWithOptions(pendingImport, indexes, at, options); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1943,27 +1951,40 @@ func insertImage(args []js.Value) (any, error) {
|
|||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveDocument 分块写出当前编辑文档
|
// editorOperationProgress 节流长操作检查点并向浏览器让出执行,保证阶段开始和结束可取消
|
||||||
// 入参: args 数据写出回调和可选的准备进度回调
|
// 入参: callback 浏览器进度回调
|
||||||
|
// 返回: func(string, int, int) error 进度回调
|
||||||
|
func editorOperationProgress(callback js.Value) func(string, int, int) error {
|
||||||
|
var lastStage string
|
||||||
|
var lastProgress time.Time
|
||||||
|
return func(stage string, completed, total int) error {
|
||||||
|
if stage == lastStage && (total == 0 || completed != total) && time.Since(lastProgress) < 32*time.Millisecond {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastStage, lastProgress = stage, time.Now()
|
||||||
|
return awaitExport(callback, stage, completed, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveDocument 分块写出当前编辑文档或指定页面
|
||||||
|
// 入参: args 数据写出回调、可选准备进度回调和可选页面索引
|
||||||
// 返回: any 保存结果, error 错误信息
|
// 返回: any 保存结果, error 错误信息
|
||||||
func saveDocument(args []js.Value) (any, error) {
|
func saveDocument(args []js.Value) (any, error) {
|
||||||
if currentEditor == nil {
|
if currentEditor == nil {
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
}
|
}
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
var lastStage string
|
currentEditor.OnWriteProgress = editorOperationProgress(args[1])
|
||||||
var lastProgress time.Time
|
|
||||||
currentEditor.OnWriteProgress = func(stage string, completed, total int) error {
|
|
||||||
if stage == lastStage && completed != total && time.Since(lastProgress) < 32*time.Millisecond {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
lastStage, lastProgress = stage, time.Now()
|
|
||||||
return awaitExport(args[1], stage, completed, total)
|
|
||||||
}
|
|
||||||
defer func() { currentEditor.OnWriteProgress = nil }()
|
defer func() { currentEditor.OnWriteProgress = nil }()
|
||||||
}
|
}
|
||||||
writer := bufio.NewWriterSize(exportWriter{write: args[0]}, 1<<20)
|
writer := bufio.NewWriterSize(exportWriter{write: args[0]}, 1<<20)
|
||||||
if _, err := currentEditor.WriteTo(writer); err != nil {
|
var err error
|
||||||
|
if len(args) > 2 {
|
||||||
|
_, err = currentEditor.WritePagesTo(writer, indexesFromJS(args[2]))
|
||||||
|
} else {
|
||||||
|
_, err = currentEditor.WriteTo(writer)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := writer.Flush(); err != nil {
|
if err := writer.Flush(); err != nil {
|
||||||
|
|||||||
+2
-1
@@ -59,10 +59,11 @@ type URI struct {
|
|||||||
Base string `xml:"Base,attr"`
|
Base string `xml:"Base,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageLink 页面矩形外链,Box使用页面毫米坐标
|
// PageLink 页面矩形链接,URI与Dest分别表示外链和文档内跳转,Box使用页面毫米坐标
|
||||||
type PageLink struct {
|
type PageLink struct {
|
||||||
URI string
|
URI string
|
||||||
Box Box
|
Box Box
|
||||||
|
Dest *Dest
|
||||||
}
|
}
|
||||||
|
|
||||||
// GotoA 附件动作
|
// GotoA 附件动作
|
||||||
|
|||||||
+61
-12
@@ -18,6 +18,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -48,11 +49,15 @@ type editorPageImport struct {
|
|||||||
defaultCS string
|
defaultCS string
|
||||||
attachments map[string][]byte
|
attachments map[string][]byte
|
||||||
copyPage bool
|
copyPage bool
|
||||||
|
progress editorProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageImportOptions 页面导入选项,零值仅导入页面及其关联资源
|
// PageImportOptions 页面导入选项,零值仅导入页面及其关联资源
|
||||||
|
// OnProgress按ids、pages、resources、commit阶段回报进度,total为0表示总量未知
|
||||||
|
// 回调返回错误则取消导入,commit检查点之后统一提交;回调不可重入修改编辑器
|
||||||
type PageImportOptions struct {
|
type PageImportOptions struct {
|
||||||
Outlines bool
|
Outlines bool
|
||||||
|
OnProgress func(stage string, completed, total int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportPages 将来源主文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
// ImportPages 将来源主文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
||||||
@@ -99,7 +104,9 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
if len(indexes) == 0 {
|
if len(indexes) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if err := e.prepareSourceIDs(); err != nil {
|
progress := editorProgress(options.OnProgress)
|
||||||
|
maximum, err := e.sourceMaxID(progress)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
base := e.source
|
base := e.source
|
||||||
@@ -109,11 +116,11 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefix := path.Join(base.directory, "Import_"+strconv.Itoa(e.maxID+1))
|
prefix := path.Join(base.directory, "Import_"+strconv.Itoa(maximum+1))
|
||||||
for editorDirectoryExists(base.reader, prefix) {
|
for editorDirectoryExists(base.reader, prefix) {
|
||||||
prefix += "_"
|
prefix += "_"
|
||||||
}
|
}
|
||||||
m := &editorPageImport{reader: source, doc: doc, prefix: prefix, maximum: e.maxID, copyPage: copyPage,
|
m := &editorPageImport{reader: source, doc: doc, prefix: prefix, maximum: maximum, copyPage: copyPage, progress: progress,
|
||||||
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)}
|
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)}
|
||||||
if copyPage {
|
if copyPage {
|
||||||
for _, page := range doc.Pages.Page {
|
for _, page := range doc.Pages.Page {
|
||||||
@@ -143,6 +150,9 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
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 {
|
||||||
|
if err := progress.report("pages", i, len(indexes)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
page := doc.Pages.Page[index]
|
page := doc.Pages.Page[index]
|
||||||
ids[i] = m.id(page.ID)
|
ids[i] = m.id(page.ID)
|
||||||
name, err := m.copyXML(source.ResPath(page.BaseLoc))
|
name, err := m.copyXML(source.ResPath(page.BaseLoc))
|
||||||
@@ -151,6 +161,9 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
}
|
}
|
||||||
refs[i] = Page{ID: ids[i], BaseLoc: name}
|
refs[i] = Page{ID: ids[i], BaseLoc: name}
|
||||||
}
|
}
|
||||||
|
if err := progress.report("pages", len(indexes), len(indexes)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
annotations, err := m.annotations()
|
annotations, err := m.annotations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -228,6 +241,9 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := progress.report("commit", 0, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if e.source == nil {
|
if e.source == nil {
|
||||||
for i := range e.resources {
|
for i := range e.resources {
|
||||||
resource := &e.resources[i]
|
resource := &e.resources[i]
|
||||||
@@ -242,6 +258,7 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.source, e.maxID = next, m.maximum
|
e.source, e.maxID = next, m.maximum
|
||||||
|
next.idsReady = true
|
||||||
e.outlines = outlines
|
e.outlines = outlines
|
||||||
e.pages = slices.Insert(e.pages, at, added...)
|
e.pages = slices.Insert(e.pages, at, added...)
|
||||||
if change := e.recordChange(); change != nil {
|
if change := e.recordChange(); change != nil {
|
||||||
@@ -266,11 +283,43 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readFile 分块读取导入条目,允许资源解压过程中取消,不共享来源内存
|
||||||
|
// 入参: name 包内路径
|
||||||
|
// 返回: []byte 条目内容, error 错误信息
|
||||||
|
func (m *editorPageImport) readFile(name string) ([]byte, error) {
|
||||||
|
input, err := m.reader.openFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
return io.ReadAll(&editorProgressReader{Reader: input, progress: m.progress, stage: "resources"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorProgressReader 为输入流提供可取消的分块读取检查点
|
||||||
|
type editorProgressReader struct {
|
||||||
|
io.Reader
|
||||||
|
progress editorProgress
|
||||||
|
stage string
|
||||||
|
completed int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read 读取数据并在下一块开始前检查取消
|
||||||
|
// 入参: data 接收缓冲区
|
||||||
|
// 返回: int 读取字节数, error 错误信息
|
||||||
|
func (r *editorProgressReader) Read(data []byte) (int, error) {
|
||||||
|
if err := r.progress.report(r.stage, r.completed, 0); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
n, err := r.Reader.Read(data[:min(len(data), 1<<20)])
|
||||||
|
r.completed += n
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
// outlines 筛选指向导入页面的目录树并复用标准动作迁移
|
// outlines 筛选指向导入页面的目录树并复用标准动作迁移
|
||||||
// 返回: []byte 目录片段, error 错误信息
|
// 返回: []byte 目录片段, error 错误信息
|
||||||
func (m *editorPageImport) outlines() ([]byte, error) {
|
func (m *editorPageImport) outlines() ([]byte, error) {
|
||||||
name := m.reader.ResPath(m.reader.OFD.DocBody[0].DocRoot)
|
name := m.reader.ResPath(m.reader.OFD.DocBody[0].DocRoot)
|
||||||
data, err := m.reader.readFile(name)
|
data, err := m.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -422,7 +471,7 @@ func (m *editorPageImport) id(value string) string {
|
|||||||
// 入参: name 资源索引路径
|
// 入参: name 资源索引路径
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (m *editorPageImport) resourceIndex(name string) error {
|
func (m *editorPageImport) resourceIndex(name string) error {
|
||||||
data, err := m.reader.readFile(name)
|
data, err := m.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -505,11 +554,11 @@ func (m *editorPageImport) copyData(name string, evidence bool) (string, error)
|
|||||||
}
|
}
|
||||||
target := path.Join(directory, name)
|
target := path.Join(directory, name)
|
||||||
if _, exists := m.files[target]; !exists {
|
if _, exists := m.files[target]; !exists {
|
||||||
data, err := m.reader.readFile(name)
|
data, err := m.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
m.files[target] = bytes.Clone(data)
|
m.files[target] = data
|
||||||
}
|
}
|
||||||
return "/" + target, nil
|
return "/" + target, nil
|
||||||
}
|
}
|
||||||
@@ -522,7 +571,7 @@ func (m *editorPageImport) copyXML(name string) (string, error) {
|
|||||||
if _, exists := m.files[target]; exists {
|
if _, exists := m.files[target]; exists {
|
||||||
return "/" + target, nil
|
return "/" + target, nil
|
||||||
}
|
}
|
||||||
data, err := m.reader.readFile(name)
|
data, err := m.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -752,7 +801,7 @@ func (m *editorPageImport) annotations() ([]byte, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
name := m.reader.ResPath(m.doc.Annotations)
|
name := m.reader.ResPath(m.doc.Annotations)
|
||||||
data, err := m.reader.readFile(name)
|
data, err := m.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -788,7 +837,7 @@ func (m *editorPageImport) attachment(id string) (string, error) {
|
|||||||
if m.doc.Attachments.Path != "" {
|
if m.doc.Attachments.Path != "" {
|
||||||
name = m.reader.ResPath(m.doc.Attachments.Path)
|
name = m.reader.ResPath(m.doc.Attachments.Path)
|
||||||
}
|
}
|
||||||
data, err := m.reader.readFile(name)
|
data, err := m.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -817,7 +866,7 @@ func (m *editorPageImport) signatures() ([]byte, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
name := m.reader.ResPath(m.doc.Signatures)
|
name := m.reader.ResPath(m.doc.Signatures)
|
||||||
data, err := m.reader.readFile(name)
|
data, err := m.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -834,7 +883,7 @@ func (m *editorPageImport) signatures() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
value, err := m.reader.readFile(loc)
|
value, err := m.readFile(loc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-7
@@ -163,9 +163,24 @@ func editorDirectoryExists(reader *Reader, directory string) bool {
|
|||||||
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID
|
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) prepareSourceIDs() error {
|
func (e *Editor) prepareSourceIDs() error {
|
||||||
if e.source == nil || e.source.idsReady {
|
maximum, err := e.sourceMaxID(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.maxID = maximum
|
||||||
|
if e.source != nil {
|
||||||
|
e.source.idsReady = true
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sourceMaxID 核对最大标识,不修改文档或缓存,允许导入准备阶段取消
|
||||||
|
// 入参: progress 进度回调
|
||||||
|
// 返回: int 最大标识, error 错误信息
|
||||||
|
func (e *Editor) sourceMaxID(progress editorProgress) (int, error) {
|
||||||
|
if e.source == nil || e.source.idsReady {
|
||||||
|
return e.maxID, nil
|
||||||
|
}
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
names := make(map[string]bool)
|
names := make(map[string]bool)
|
||||||
for name := range reader.fileIndex {
|
for name := range reader.fileIndex {
|
||||||
@@ -181,17 +196,16 @@ func (e *Editor) prepareSourceIDs() error {
|
|||||||
}
|
}
|
||||||
input, err := reader.openFile(name)
|
input, err := reader.openFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
id, err := editorXMLMaxID(input)
|
id, err := editorXMLMaxID(&editorProgressReader{Reader: input, progress: progress, stage: "ids"})
|
||||||
input.Close()
|
input.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("scan IDs in %s: %w", name, err)
|
return 0, fmt.Errorf("scan IDs in %s: %w", name, err)
|
||||||
}
|
}
|
||||||
maximum = max(maximum, id)
|
maximum = max(maximum, id)
|
||||||
}
|
}
|
||||||
e.maxID, e.source.idsReady = maximum, true
|
return maximum, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件
|
// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件
|
||||||
@@ -202,7 +216,8 @@ func editorXMLMaxID(input io.Reader) (int, error) {
|
|||||||
maximum, started := 0, false
|
maximum, started := 0, false
|
||||||
for {
|
for {
|
||||||
token, err := decoder.Token()
|
token, err := decoder.Token()
|
||||||
if err == io.EOF || err != nil && !started {
|
var syntax *xml.SyntaxError
|
||||||
|
if err == io.EOF || !started && errors.As(err, &syntax) {
|
||||||
return maximum, nil
|
return maximum, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -475,9 +475,10 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩
|
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩
|
||||||
|
// 入参: progress 准备进度回调
|
||||||
// 返回: *Reader 阅读器, error 错误信息
|
// 返回: *Reader 阅读器, error 错误信息
|
||||||
func (e *Editor) sourceReader() (*Reader, error) {
|
func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
|
||||||
parts, err := e.sourceParts(nil)
|
parts, err := e.sourceParts(progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-3
@@ -180,9 +180,9 @@ func (r *Renderer) renderPageToContext(ctx *canvas.Context, page *PageContent, d
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageLinks 获取页面及可见注释的矩形外链,不包含自定义Region
|
// PageLinks 获取页面及可见注释的矩形链接,解析书签目标,不包含自定义Region
|
||||||
// 入参: page 页面内容
|
// 入参: page 页面内容
|
||||||
// 返回: []PageLink 页面外链, error 错误信息
|
// 返回: []PageLink 页面链接, error 错误信息
|
||||||
func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
||||||
box, err := r.GetPageBox(page)
|
box, err := r.GetPageBox(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,12 +193,31 @@ func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
|||||||
sources = append(sources, annotationActionSources(r.Reader.Annots[page.ID])...)
|
sources = append(sources, annotationActionSources(r.Reader.Annots[page.ID])...)
|
||||||
}
|
}
|
||||||
var links []PageLink
|
var links []PageLink
|
||||||
|
var bookmarks map[string]Dest
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
if source.Box.W <= 0 || source.Box.H <= 0 {
|
if source.Box.W <= 0 || source.Box.H <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, action := range source.Actions {
|
for _, action := range source.Actions {
|
||||||
if action.Event == "CLICK" && action.URI != nil && action.URI.URI != "" && action.Region == nil {
|
if action.Event != "CLICK" || action.Region != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if action.Goto != nil {
|
||||||
|
if bookmarks == nil {
|
||||||
|
doc, err := r.Reader.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bookmarks = make(map[string]Dest, len(doc.Bookmarks.Bookmark))
|
||||||
|
for _, bookmark := range doc.Bookmarks.Bookmark {
|
||||||
|
bookmarks[bookmark.Name] = bookmark.Dest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
||||||
|
value := *dest
|
||||||
|
links = append(links, PageLink{Dest: &value, Box: source.Box})
|
||||||
|
}
|
||||||
|
} else if action.URI != nil && action.URI.URI != "" {
|
||||||
links = append(links, PageLink{URI: resolveActionURI(*action.URI), Box: source.Box})
|
links = append(links, PageLink{URI: resolveActionURI(*action.URI), Box: source.Box})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-2
@@ -86,21 +86,57 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
|||||||
return output.count, err
|
return output.count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WritePagesTo 按指定顺序将页面另存为独立OFD,不修改当前文档或撤销记录
|
||||||
|
// 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转
|
||||||
|
// 签名凭据不代表另存文件验签有效;进度与取消复用OnWriteProgress,另含snapshot、ids和commit阶段
|
||||||
|
// 入参: writer 输出流, indexes 从0开始的页面索引,不可重复或为空
|
||||||
|
// 返回: int64 已写入字节数, error 错误信息
|
||||||
|
func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (int64, error) {
|
||||||
|
if len(indexes) == 0 {
|
||||||
|
return 0, fmt.Errorf("no pages selected")
|
||||||
|
}
|
||||||
|
progress := editorProgress(e.OnWriteProgress)
|
||||||
|
if err := progress.report("snapshot", 0, 0); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
reader, err := e.reader(progress)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
selected := NewEditor()
|
||||||
|
id := selected.Info.DocID
|
||||||
|
selected.Info = cloneEditorData(e.Info)
|
||||||
|
selected.Info.DocID = id
|
||||||
|
selected.OnWriteProgress = e.OnWriteProgress
|
||||||
|
if _, err := selected.ImportPagesWithOptions(reader, indexes, 0, PageImportOptions{Outlines: true, OnProgress: e.OnWriteProgress}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return selected.WriteTo(writer)
|
||||||
|
}
|
||||||
|
|
||||||
// Reader 获取当前文档的独立内存快照,不进行ZIP压缩,后续修改不影响已有快照
|
// Reader 获取当前文档的独立内存快照,不进行ZIP压缩,后续修改不影响已有快照
|
||||||
// 返回的Reader可用于现有渲染、搜索和导出接口,新增资源仅包含实际引用的部分,二进制数据内部共享只读
|
// 返回的Reader可用于现有渲染、搜索和导出接口,新增资源仅包含实际引用的部分,二进制数据内部共享只读
|
||||||
// 返回: *Reader 阅读器, error 错误信息
|
// 返回: *Reader 阅读器, error 错误信息
|
||||||
func (e *Editor) Reader() (*Reader, error) {
|
func (e *Editor) Reader() (*Reader, error) {
|
||||||
|
return e.reader(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reader 生成内存快照,保存时复用准备进度,预览时不触发回调
|
||||||
|
// 入参: progress 准备进度回调
|
||||||
|
// 返回: *Reader 阅读器, error 错误信息
|
||||||
|
func (e *Editor) reader(progress editorProgress) (*Reader, error) {
|
||||||
if err := e.validate(); err != nil {
|
if err := e.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
return e.sourceReader()
|
return e.sourceReader(progress)
|
||||||
}
|
}
|
||||||
r := &Reader{files: make(map[string][]byte)}
|
r := &Reader{files: make(map[string][]byte)}
|
||||||
if err := e.writeParts(func(name string, data []byte, _ bool) error {
|
if err := e.writeParts(func(name string, data []byte, _ bool) error {
|
||||||
r.files[name] = data
|
r.files[name] = data
|
||||||
return nil
|
return nil
|
||||||
}, nil); err != nil {
|
}, progress); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := r.initRoot(); err != nil {
|
if err := r.initRoot(); err != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user