feat(加载取消): 统一文档准备与取消流程
build.yaml / build (push) Successful in 49s

This commit is contained in:
xiaoqidun committed 2026-10-26 18:52:35 +08:00
1 parent fb4f03465b
commit 413d41c31d
7 files changed
+261 -73

No files matched your search

+107 -29
View File
@@ -674,7 +674,7 @@ function createPrintDialog(tab) {
show: panel => documentDialogs.openFor(tab, panel),
download: (bytes, mime, name) => nativeHost ? nativeHost.download(bytes, mime, name) : downloadBytes(bytes, mime, name),
cancel: tab => {
for (const [id, request] of wasmRequests) if (request.tab === tab && ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF"].includes(request.name)) tab.worker?.postMessage({type:"cancel", id});
for (const [id, request] of wasmRequests) if (request.tab === tab && ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF"].includes(request.name)) cancelOperation(id);
},
});
}
@@ -886,7 +886,7 @@ resourceElements.Form.addEventListener("submit", event => event.preventDefault()
resourceElements.Panel.addEventListener("close", () => {
rememberResourceView();
resourceManager.sequence++;
if (resourceManager.requestID) wasmWorker?.postMessage({type: "cancel", id: resourceManager.requestID});
if (resourceManager.requestID) cancelOperation(resourceManager.requestID);
releaseResourcePreview();
});
resourceElements.List.closest(".resource-list-container").addEventListener("scroll", renderResourceWindow, {passive: true});
@@ -2633,6 +2633,7 @@ async function closeWorkspace() {
if (workspace.closing) return;
workspace.closing = true;
try {
for (const tab of workspace.tabs) cancelDocumentPreparation(tab);
await Promise.all(workspace.tabs.filter(tab => tab.state.saving && tab.saveCompletion).map(tab => tab.saveCompletion));
if (workspace.tabs.some(tab => tab.state.saving)) { setStatus("文档正在保存,请稍后关闭"); return; }
if (batch.running && !window.confirm("批量转换尚未完成,是否退出?")) return;
@@ -3080,6 +3081,7 @@ async function closeFile(tab = documentTab(), keepTab = false) {
queueMicrotask(() => { if (workspace.closeDefault === tab) workspace.closeDefault = null; });
}
try {
cancelDocumentPreparation(tab);
if (tab.state.saving && tab.saveCompletion) await tab.saveCompletion;
if (tab.closed) return;
if (tab.opening || tab.state.exporting) {
@@ -5487,8 +5489,6 @@ async function loadWASM(tab = documentTab()) {
}
const visible = isForegroundDocument(tab);
if (data.type === "progress") tab.progress = {text: data.text, percent: data.percent};
if (data.type === "conversion") tab.progress = {text: data.phase === "convert" ? `正在转换页面 · ${data.completed} / ${data.total}` : "正在转换文档",
percent: data.total ? 28 + data.completed / data.total * 22 : null};
if (data.type === "font") {
try {
const request = wasmRequests.get(data.id);
@@ -5505,11 +5505,13 @@ async function loadWASM(tab = documentTab()) {
if (data.type === "progress") {
if (visible) setProgress(data.text, data.percent);
} else if (data.type === "conversion") {
if (visible && wasmRequests.get(data.id)?.name !== "ofdgoLoadPDF" && wasmRequests.get(data.id)?.openSeq === state.openSeq && !el.cancelExportButton.disabled) {
if (data.phase === "open") setProgress("正在读取文档", null);
else if (data.phase === "pages") setProgress(data.completed ? `正在整理页面 · ${data.completed} 页` : "正在整理页面", null);
else if (data.phase === "convert") setProgress(`正在转换页面 · ${data.completed} / ${data.total} 页`, data.total ? 28 + data.completed / data.total * 22 : null);
else setProgress("正在生成文档", null);
const request = wasmRequests.get(data.id);
if (request?.name === "ofdgoLoadPDF") {
for (const pending of wasmRequests.values()) if (pending.tab === tab && pending.preparing) operationProgress(pending, data);
} else if (request?.name === "ofdgoOpenPDF") {
const text = data.phase === "open" ? "正在读取文档" : data.phase === "pages" ? "正在整理页面" : "正在准备阅读";
tab.progress = {text, percent:null};
if (visible && !el.cancelExportButton.disabled) setProgress(text, null);
}
} else if (data.type === "export") {
if (data.id === state.saveRequestID && state.saving && !state.exporting) {
@@ -5555,6 +5557,7 @@ async function loadWASM(tab = documentTab()) {
} else {
const request = wasmRequests.get(data.id);
if (!request || request.tab !== tab) return;
if (data.code === "documentPending" && !request.canceled) { prepareOperation(data.id, request); return; }
wasmRequests.delete(data.id);
if (data.ok) {
request.resolve(data.data);
@@ -5563,7 +5566,7 @@ async function loadWASM(tab = documentTab()) {
err.code = data.code;
if (data.errorName) err.name = data.errorName;
err.missingGlyphs = data.missingGlyphs;
if (data.canceled) {
if (data.canceled || request.canceled) {
err.name = "AbortError";
}
request.reject(err);
@@ -6317,6 +6320,7 @@ async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, import
try {
if (pdf) {
state.pdfComplete = false;
tab.pdfTask = null;
return await callWASM("ofdgoOpenPDF", data, fonts, credentials?.password || null, state.renderAnnotations);
}
return importing ? await callWASM("ofdgoLoadImport", data?.nativeFile ? data : data.slice(), credentials)
@@ -6526,33 +6530,36 @@ async function openDocument(options = {}) {
}
}
async function completePDF(tab) {
function completePDF(tab) {
const state = tab.state, source = state.ofdBytes, wasmSeq = state.wasmSeq;
if (tab.pdfTask?.wasmSeq === wasmSeq || state.pdfComplete || !state.sourcePDF || !source || state.editorInfo) return;
const task = {wasmSeq};
if (state.pdfComplete || !state.sourcePDF || state.editorInfo) return Promise.resolve();
if (tab.pdfTask?.wasmSeq === wasmSeq && tab.pdfTask.source === source) return tab.pdfTask.promise;
const task = {wasmSeq, source};
tab.pdfTask = task;
const current = () => !tab.closed && state.ofdBytes === source && state.wasmSeq === wasmSeq;
try {
const current = () => !tab.closed && tab.pdfTask === task && state.ofdBytes === source && state.wasmSeq === wasmSeq;
task.promise = (async () => {
while (current()) {
await new Promise(resolve => setTimeout(resolve, 0));
if (!current()) return;
if (!current()) break;
const result = await callTabWASM(tab, "ofdgoLoadPDF");
if (!current()) return;
if (!current()) break;
if (!result.done) continue;
state.pdfComplete = true;
tab.pdfTask = null;
state.conversionWarnings = JSON.parse(result.warnings || "null") || [];
if (!state.editorInfo) Object.assign(state.doc, result.doc);
if (isForegroundDocument(tab)) loadDocumentDetails(state.openSeq);
return;
}
} catch (err) {
if (current() && !err.canceled) {
throw new DOMException("文档已关闭", "AbortError");
})();
task.promise.catch(err => {
if (current() && err.name !== "AbortError") {
tab.status = err.message;
if (isForegroundDocument(tab)) showError(err, false);
}
} finally {
if (tab.pdfTask === task) tab.pdfTask = null;
}
});
return task.promise;
}
async function loadDocumentDetails(openSeq) {
@@ -10065,7 +10072,7 @@ function cancelExport() {
}
const tab = documentTab();
const target = tab.state;
const requestID = target.exportRequestID || target.saveRequestID;
const requestID = operationRequest(tab)?.id || target.exportRequestID || target.saveRequestID;
if (!requestID || el.cancelExportButton.disabled) {
return;
}
@@ -10078,7 +10085,7 @@ function cancelExport() {
el.importStatus.textContent = "正在取消操作";
}
setProgress("正在取消操作", null);
tab.worker.postMessage({ type: "cancel", id: requestID });
cancelOperation(requestID);
}
function releaseMediaPlayers(surface) {
@@ -10092,7 +10099,7 @@ function releaseMediaPlayer(id, entry) {
if (state.mediaPlayers?.get(id) !== entry) return;
state.mediaPlayers.delete(id);
for (const [requestID, request] of wasmRequests) {
if (request.mediaID === id) wasmWorker?.postMessage({type: "cancel", id: requestID});
if (request.tab === documentTab() && request.mediaID === id) cancelOperation(requestID);
}
entry.finish?.();
closeMediaWindow(entry);
@@ -12348,6 +12355,72 @@ async function callWASM(name, ...args) {
return callTabWASM(documentTab(), name, ...args);
}
function operationRequest(tab) {
return [...wasmRequests.values()].findLast(request => request.tab === tab && request.cancellable && !request.name.includes("Print"));
}
function operationProgress(request, progress) {
const text = request.name.includes("Print") ? "正在准备打印" : request.name === "ofdgoEditDocument" ? "正在准备编辑" : request.name.includes("Save") ? "正在准备保存" : request.name.includes("Export") ? "正在准备导出" : "正在准备文档";
const completed = progress?.phase === "convert" && progress.total > 0;
const label = completed ? `${text} · ${progress.completed} / ${progress.total} 页` : text;
if (request.name.includes("Print")) {
if (request.tab.print?.context) request.tab.print.status(label, true);
} else if (operationRequest(request.tab) === request) {
request.tab.progress = {text:label, percent:completed ? progress.completed / progress.total * 100 : null};
if (isForegroundDocument(request.tab)) setProgress(label, request.tab.progress.percent);
}
}
function finishOperation(request) {
if (request.tab.preparationBusy && !operationRequest(request.tab)) {
request.tab.preparationBusy = false;
if (!request.tab.closed) setBusy(false, "", 0, "", request.tab);
}
if (request.cancellable && isForegroundDocument(request.tab) && !operationRequest(request.tab)) {
el.cancelExportButton.hidden = !request.tab.state.saving && !request.tab.state.exporting;
}
}
function cancelDocumentPreparation(tab) {
if (!tab.state.sourcePDF || tab.state.pdfComplete) return;
for (const [id, request] of wasmRequests) if (request.tab === tab && (request.preparing || request.name === "ofdgoPrepareSave")) cancelOperation(id);
}
function cancelOperation(id) {
const request = wasmRequests.get(id);
if (!request) return;
request.canceled = true;
if (request.preparing) {
wasmRequests.delete(id);
request.reject(new DOMException("操作已取消", "AbortError"));
} else request.tab.worker?.postMessage({type:"cancel", id});
}
async function prepareOperation(id, request) {
request.preparing = true;
request.cancellable = true;
const tab = request.tab;
if (!request.name.includes("Print")) {
if (!tab.busy) tab.preparationBusy = true;
setBusy(true, tab.progress?.text || "正在准备文档", null, "", tab);
if (isForegroundDocument(tab)) {
el.cancelExportButton.hidden = false;
el.cancelExportButton.disabled = false;
}
}
operationProgress(request);
try {
await completePDF(tab);
if (wasmRequests.get(id) !== request || request.canceled || tab.closed) return;
request.preparing = false;
request.send();
} catch (err) {
if (wasmRequests.get(id) !== request) return;
wasmRequests.delete(id);
request.reject(err);
}
}
async function callTabWASM(tab, name, ...args) {
const state = tab.state;
const visible = isForegroundDocument(tab);
@@ -12369,7 +12442,10 @@ async function callTabWASM(tab, name, ...args) {
}
}
if (name === "ofdgoResources" && workspace.active === tab && resourceElements.Panel.open) resourceManager.requestID = id;
wasmRequests.set(id, { name, resolve, reject, openSeq: state.openSeq, tab });
const request = {name, openSeq:state.openSeq, tab, id, cancellable:name === "ofdgoEditDocument",
resolve:value => { finishOperation(request); resolve(value); }, reject:err => { finishOperation(request); reject(err); }};
wasmRequests.set(id, request);
if (request.cancellable && visible) { el.cancelExportButton.hidden = false; el.cancelExportButton.disabled = false; }
if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0];
if (name === "ofdgoOpenPDF" || state.sourcePDF && !state.editorInfo) {
const request = wasmRequests.get(id);
@@ -12394,10 +12470,11 @@ async function callTabWASM(tab, name, ...args) {
const message = {id, name, args};
if (name === "ofdgoLoadPDF") message.background = true;
if (name === "ofdgoRenderPage" && [...state.pageInFlight.values()].some(task => task.index === args[0] && pageView.pageRenderPriority(task, state) >= 6)) message.preload = true;
tab.worker.postMessage(message, transfer);
request.send = () => tab.worker.postMessage(message, transfer);
request.send();
} catch (err) {
wasmRequests.delete(id);
reject(err);
request.reject(err);
}
});
}
@@ -12411,7 +12488,7 @@ function showError(err, empty = !state.doc) {
}
function setBusy(busy, text = "", percent = 0, status = "", tab = documentTab()) {
busy ||= Boolean(tab.editTask);
busy ||= Boolean(tab.editTask || operationRequest(tab));
tab.busy = busy;
if (text) tab.progress = {text, percent};
if (status) tab.status = status;
@@ -12468,6 +12545,7 @@ function setBusy(busy, text = "", percent = 0, status = "", tab = documentTab())
pageView.endPan();
el.progressPanel.hidden = !text;
setProgress(text, percent, status);
if (operationRequest(tab)) { el.cancelExportButton.hidden = false; el.cancelExportButton.disabled = false; }
}
function setProgress(text = "", percent = 0, status = "", tab = documentTab()) {
+14 -6
View File
@@ -45,7 +45,7 @@ self.onmessage = ({ data }) => {
if (engine.closed) return;
const opened = JSON.parse(globalThis.ofdgoEngineOpen(engineID));
if (!opened.ok) throw new Error(opened.error);
const backends = JSON.parse(await globalThis.ofdgoEngineCall(engineID, "ofdgoRenderBackends"));
const backends = JSON.parse(await globalThis.ofdgoEngineCall(engineID, "ofdgoRenderBackends", null));
if (!backends.ok) throw new Error(backends.error);
if (!engine.closed) self.postMessage({ engineID, type: "ready", backends: backends.data, memory: engineMemory.buffer.byteLength });
}).catch(error => {
@@ -141,7 +141,7 @@ async function handleBatchMessage(engine, { id, name, args }) {
if (name === "ofdgoConvertFile") {
const [file, options, fonts, destination] = args;
credentials = options.credentials;
const formats = JSON.parse(await globalThis.ofdgoEngineCall(engine.id, "ofdgoOutputFormats")).data;
const formats = JSON.parse(await globalThis.ofdgoEngineCall(engine.id, "ofdgoOutputFormats", null)).data;
const format = formats.find(item => item.value === options.format);
if (!format) throw new Error("不支持此格式");
root = destination;
@@ -178,7 +178,7 @@ async function handleBatchMessage(engine, { id, name, args }) {
chunks = [];
}
};
payload = await globalThis.ofdgoEngineCall(engine.id, "ofdgoConvertFile", read(file), file.size, options, write,
payload = await globalThis.ofdgoEngineCall(engine.id, "ofdgoConvertFile", done => finish(done), read(file), file.size, options, write,
(phase, completed, total, done) => {
postMessage({ id, type: "conversion", phase, completed, total, memory: engineMemory.buffer.byteLength });
finish(done);
@@ -188,7 +188,7 @@ async function handleBatchMessage(engine, { id, name, args }) {
} else {
const [entries, handle, compression] = args;
if (handle) output = await handle.createWritable();
payload = await globalThis.ofdgoEngineCall(engine.id, "ofdgoPackFiles", entries.map(entry => ({ name: entry.name, size: entry.file.size })),
payload = await globalThis.ofdgoEngineCall(engine.id, "ofdgoPackFiles", done => finish(done), entries.map(entry => ({ name: entry.name, size: entry.file.size })),
index => read(entries[index].file), write,
(completed, total, done) => { postMessage({ id, type: "conversion", phase: "pack", completed, total, memory: engineMemory.buffer.byteLength }); finish(done); },
done => finish(done), compression);
@@ -240,6 +240,10 @@ async function handleMessage(engine, { id, name, args }) {
const converting = name === "ofdgoOpenPDF";
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSnapshot" || name === "ofdgoSaveSigned";
const resources = ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoResources", "ofdgoPrepareSave"].includes(name);
if (engine.pdf) {
const checked = JSON.parse(await globalThis.ofdgoEngineCall(engine.id, "ofdgoCheckDocument", null, name));
if (!checked.ok) { postMessage({id, ...checked}); operations.delete(id); return; }
}
const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null;
const keyPassword = name === "ofdgoOpen" ? args[3]?.keyPassword : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.keyPassword : null;
const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null;
@@ -339,7 +343,7 @@ async function handleMessage(engine, { id, name, args }) {
}
if (engine.pdf && name !== "ofdgoOpenPDF") {
channel ||= new MessageChannel();
const context = await globalThis.ofdgoEngineCall(engine.id, "ofdgoPDFContext", (phase, completed, total, done) => {
const context = await globalThis.ofdgoEngineCall(engine.id, "ofdgoPDFContext", null, (phase, completed, total, done) => {
postMessage({id, type: "conversion", phase, completed, total});
channel.port1.onmessage = () => done("", signal.aborted);
channel.port2.postMessage(null);
@@ -347,7 +351,11 @@ async function handleMessage(engine, { id, name, args }) {
engine.pdf = JSON.parse(context).data === true;
}
const started = name === "ofdgoOpen" || name === "ofdgoRenderPage" ? performance.now() : null;
const payload = await globalThis.ofdgoEngineCall(engine.id, name, ...args);
const payload = await globalThis.ofdgoEngineCall(engine.id, name, done => {
channel ||= new MessageChannel();
channel.port1.onmessage = () => done("", signal.aborted);
channel.port2.postMessage(null);
}, ...args);
signal?.throwIfAborted();
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
if (result.ok && name === "ofdgoOpenPDF") engine.pdf = true;
+45 -10
View File
@@ -1591,14 +1591,31 @@ func (e *businessEngine) loadPDF(args []platformValue) (any, error) {
return map[string]any{"done": true, "doc": e.currentSession.Summary(), "warnings": string(warnings)}, nil
}
// preparePDF 按命令准备页面快照或完整文档,不将未完成内容交给编辑和输出
// checkDocument 检查命令所需内容,不隐式转换或产生输出
// 入参: name 命令名称
// 返回: error 文档尚未完整时返回ErrIncompleteDocument
func (e *businessEngine) checkDocument(name string) error {
if e.pdf == nil {
return nil
}
switch name {
case "ofdgoCheckDocument", "ofdgoPDFContext", "ofdgoOpenPDF", "ofdgoOpen", "ofdgoLoadPDF", "ofdgoConfigure", "ofdgoDocumentInfo", "ofdgoSVGFontData", "ofdgoRenderBackends", "ofdgoOutputFormats", "ofdgoExportFormats", "ofdgoParsePageRange", "ofdgoMatchFontFiles", "ofdgoOutlineLinks", "ofdgoDocumentNavigation", "ofdgoInvoices", "ofdgoRenderPage", "ofdgoSearchPage", "ofdgoPageTextString", "ofdgoOpenLinks", "ofdgoDiscardPrint", "ofdgoDiscardSave", "ofdgoDetectFormat", "ofdgoConvertFile", "ofdgoPackFiles", "ofdgoLoadImport", "ofdgoCreateDocument", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF":
return nil
default:
return ofdgo.ErrIncompleteDocument
}
}
// preparePDF 只为页面读取和后台加载绑定转换上下文,不补全其他命令
// 入参: name 命令名称, args 命令参数
// 返回: error 转换或取消错误
func (e *businessEngine) preparePDF(name string, args []platformValue) error {
if e.pdf == nil {
return nil
}
if name == "ofdgoPDFContext" || name == "ofdgoOpenPDF" || name == "ofdgoOpen" {
switch name {
case "ofdgoLoadPDF", "ofdgoRenderPage", "ofdgoSearchPage", "ofdgoPageTextString", "ofdgoOpenLinks":
default:
return nil
}
e.pdf.operation, e.pdf.report = e.newEditorOperationContext(e.pdf.progress)
@@ -1619,7 +1636,7 @@ func (e *businessEngine) preparePDF(name string, args []platformValue) error {
}
}
switch name {
case "ofdgoLoadPDF", "ofdgoConfigure", "ofdgoDocumentInfo", "ofdgoSVGFontData", "ofdgoRenderBackends", "ofdgoOutputFormats", "ofdgoExportFormats", "ofdgoParsePageRange", "ofdgoMatchFontFiles", "ofdgoOutlineLinks", "ofdgoDocumentNavigation", "ofdgoInvoices":
case "ofdgoLoadPDF":
return nil
case "ofdgoOpenLinks":
if len(args) == 0 {
@@ -1644,8 +1661,7 @@ func (e *businessEngine) preparePDF(name string, args []platformValue) error {
e.pdf.page = index
return nil
default:
_, err := e.finishPDF()
return err
return nil
}
}
@@ -5950,7 +5966,11 @@ func (e *businessEngine) createDocument(args []platformValue) (any, error) {
return nil, err
}
editor.SetHistoryLimit(100)
return e.previewEditor(editor, args[3].Bool())
info, err := e.previewEditor(editor, args[3].Bool())
if err == nil {
e.clearPDF()
}
return info, err
}
// updateInfo 修改完整文档描述,按字段来源保留自定义内容的扩展属性
@@ -6044,7 +6064,7 @@ func (e *businessEngine) documentVersions(args []platformValue) (any, error) {
return nil, err
}
}
versions, err := reader.DocumentVersions()
versions, err := reader.DocumentVersionsContext(e.operationContext())
if err != nil {
return nil, err
}
@@ -6635,7 +6655,8 @@ func (e *businessEngine) editDocument(args []platformValue) (any, error) {
e.currentSession.editing = true
return e.editorSummary(), nil
}
editor, err := e.currentSession.Reader.Editor()
ctx := e.operationContext()
editor, err := e.currentSession.Reader.EditorContext(ctx)
if err != nil {
return nil, err
}
@@ -6648,13 +6669,19 @@ func (e *businessEngine) editDocument(args []platformValue) (any, error) {
if len(args) != 0 {
page = args[0].Int()
}
content, err := editor.Page(page)
content, err := editor.PageContext(ctx, page)
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
if err := e.syncEditorPageIDs(page, content); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
e.currentEditor = editor
e.currentSession.editor = editor
e.currentSession.editing = true
@@ -7557,7 +7584,7 @@ func (e *businessEngine) saveDocument(args []platformValue) (any, error) {
if e.currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
editor, err := e.currentSession.Reader.Editor()
editor, err := e.currentSession.Reader.EditorContext(e.operationContext())
if err != nil {
return nil, err
}
@@ -7662,6 +7689,12 @@ func (c *outputContext) Err() error {
// businessCommands 共用业务命令表
func (e *businessEngine) businessCommands() map[string]func([]platformValue) (any, error) {
return map[string]func([]platformValue) (any, error){
"ofdgoCheckDocument": func(args []platformValue) (any, error) {
if len(args) != 1 {
return nil, fmt.Errorf("missing operation name")
}
return nil, e.checkDocument(args[0].String())
},
"ofdgoPreparePrint": e.preparePrint,
"ofdgoLayoutPrint": e.layoutPrint,
"ofdgoPrintSide": e.printSide,
@@ -7818,6 +7851,8 @@ func (e *businessEngine) operationContext() context.Context {
func failureResult(err error) apiResult {
result := apiResult{Error: err.Error()}
switch {
case errors.Is(err, ofdgo.ErrIncompleteDocument):
result.Code, result.Error = "documentPending", "文档尚未准备完成"
case errors.Is(err, ofdgo.ErrPrintDenied):
result.Code, result.Error = "printDenied", "文档禁止打印"
case errors.Is(err, ofdgo.ErrPDFPassword):
+3
View File
@@ -311,6 +311,9 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
if fn == nil {
return nativeFailure(fmt.Errorf("unknown operation: %s", name))
}
if err := e.checkDocument(name); err != nil {
return nativeFailure(err)
}
call := &nativeCall{engine: e, ctx: ctx, emit: emit}
if e.pdf != nil {
e.pdf.progress = platformValueOf(call.progress("conversion", "", true))
+30 -14
View File
@@ -68,32 +68,48 @@ func (w *wasmEngines) open(args []js.Value) (any, error) {
return nil, nil
}
// call 按会话标识分派业务命令,不提供默认会话
// 入参: args 会话标识、命令及业务参数
// call 按会话标识分派业务命令,统一绑定本次调用的取消上下文
// 入参: args 会话标识、命令、取消检查点及业务参数
// 返回: any 业务结果, error 调用错误
func (w *wasmEngines) call(args []js.Value) (any, error) {
if len(args) < 2 || args[0].Type() != js.TypeString || args[1].Type() != js.TypeString {
if len(args) < 3 || args[0].Type() != js.TypeString || args[1].Type() != js.TypeString {
return nil, fmt.Errorf("missing engine ID or operation")
}
e := w.engines[args[0].String()]
if e == nil {
return nil, fmt.Errorf("unknown engine: %s", args[0].String())
}
if e.pdf != nil {
defer func() {
if e.currentSession != nil {
e.currentSession.Renderer.OutputContext = context.Background()
}
}()
}
fn := e.businessCommands()[args[1].String()]
name := args[1].String()
fn := e.businessCommands()[name]
if fn == nil {
return nil, fmt.Errorf("unknown operation: %s", args[1].String())
return nil, fmt.Errorf("unknown operation: %s", name)
}
if err := e.preparePDF(args[1].String(), args[2:]); err != nil {
if err := e.checkDocument(name); err != nil {
return nil, err
}
return fn(args[2:])
e.ctx = context.Background()
if args[2].Type() == js.TypeFunction {
e.ctx = e.newOutputContext(args[2])
}
ctx := e.ctx
if name != "ofdgoSaveSnapshot" && e.currentSession != nil {
e.currentSession.Renderer.OutputContext = ctx
}
defer func() {
if e.ctx == ctx {
e.ctx = nil
}
if name != "ofdgoSaveSnapshot" && e.currentSession != nil {
e.currentSession.Renderer.OutputContext = context.Background()
}
}()
if err := ctx.Err(); err != nil {
return nil, err
}
if err := e.preparePDF(name, args[3:]); err != nil {
return nil, err
}
return fn(args[3:])
}
// close 释放会话的文档、编辑器、导入及剪贴板,不关闭共享资源
+22 -2
View File
@@ -223,7 +223,14 @@ func (e *Editor) SetInfo(info DocInfo) {
// 入参: index 页面索引
// 返回: *PageContent 页面内容, error 错误信息
func (e *Editor) Page(index int) (*PageContent, error) {
page, err := e.page(index)
return e.PageContext(context.Background(), index)
}
// PageContext 获取独立页面副本,读取、解析及整理对象时响应取消
// 入参: ctx 取消上下文, index 页面索引
// 返回: *PageContent 页面内容, error 读取或取消错误
func (e *Editor) PageContext(ctx context.Context, index int) (*PageContent, error) {
page, err := e.pageContext(ctx, index)
if err != nil {
return nil, err
}
@@ -233,6 +240,9 @@ func (e *Editor) Page(index int) (*PageContent, error) {
layer := &result.Content.Layer[i]
layer.TextObject, layer.PathObject, layer.ImageObject, layer.CompositeGraphicUnit = nil, nil, nil, nil
for _, object := range layer.Objects {
if err := ctx.Err(); err != nil {
return nil, err
}
switch object.Type {
case "TextObject":
object.TextObject.layout = nil
@@ -820,11 +830,21 @@ func (r editorResource) definition() string {
// 入参: index 页面索引
// 返回: *PageContent 页面内容, error 错误信息
func (e *Editor) page(index int) (*PageContent, error) {
return e.pageContext(context.Background(), index)
}
// pageContext 按索引加载内部页面并响应取消
// 入参: ctx 取消上下文, index 页面索引
// 返回: *PageContent 页面内容, error 读取或取消错误
func (e *Editor) pageContext(ctx context.Context, index int) (*PageContent, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if index < 0 || index >= len(e.pages) {
return nil, fmt.Errorf("page index %d out of range", index)
}
if e.source != nil {
if err := e.loadSourcePage(index); err != nil {
if err := e.loadSourcePageContext(ctx, index); err != nil {
return nil, err
}
}
+40 -12
View File
@@ -397,15 +397,18 @@ func (e *Editor) sourceRGB() bool {
}
// repairSourceObjectIDs 按实际节点区分重复对象,只修复没有引用歧义的编号
// 入参: data 页面原文, root 页面节点
// 入参: ctx 取消上下文, data 页面原文, root 页面节点
// 返回: []byte 编辑副本, bool 是否修复, error 错误信息
func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bool, error) {
func (e *Editor) repairSourceObjectIDs(ctx context.Context, data []byte, root *editorXML) ([]byte, bool, error) {
seen := make(map[string]bool)
duplicates := make(map[string]bool)
var nodes []*editorXML
var visit func(*editorXML) error
visit = func(parent *editorXML) error {
for _, node := range parent.children {
if err := ctx.Err(); err != nil {
return err
}
if !ofdXMLChild(parent.name, node.name) {
continue
}
@@ -440,15 +443,18 @@ func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bo
if len(nodes) == 0 {
return data, false, nil
}
if err := e.checkDuplicateObjectReferences(duplicates); err != nil {
if err := e.checkDuplicateObjectReferences(ctx, duplicates); err != nil {
return nil, false, err
}
if err := e.prepareSourceIDs(); err != nil {
if err := e.prepareSourceIDsContext(ctx); err != nil {
return nil, false, err
}
maximum := e.maxID
var patches []editorXMLPatch
for _, node := range nodes {
if err := ctx.Err(); err != nil {
return nil, false, err
}
maximum++
encoded, err := editorXMLAttribute(data, node, "ID", strconv.Itoa(maximum))
if err != nil {
@@ -461,9 +467,9 @@ func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bo
}
// checkDuplicateObjectReferences 检查编号引用的目标歧义
// 入参: duplicates 重复编号
// 入参: ctx 取消上下文, duplicates 重复编号
// 返回: error 引用歧义或读取错误
func (e *Editor) checkDuplicateObjectReferences(duplicates map[string]bool) error {
func (e *Editor) checkDuplicateObjectReferences(ctx context.Context, duplicates map[string]bool) error {
reader := e.source.reader
for name := range reader.fileNames {
if !strings.EqualFold(path.Ext(name), ".xml") {
@@ -475,6 +481,10 @@ func (e *Editor) checkDuplicateObjectReferences(duplicates map[string]bool) erro
}
decoder := xml.NewDecoder(input)
for {
if err := ctx.Err(); err != nil {
input.Close()
return err
}
token, readErr := decoder.Token()
if readErr != nil {
input.Close()
@@ -500,6 +510,16 @@ func (e *Editor) checkDuplicateObjectReferences(duplicates map[string]bool) erro
// 入参: index 页面索引
// 返回: error 错误信息
func (e *Editor) loadSourcePage(index int) error {
return e.loadSourcePageContext(context.Background(), index)
}
// loadSourcePageContext 解析原始页面,取消时不提交未完成页面
// 入参: ctx 取消上下文, index 页面索引
// 返回: error 读取、解析或取消错误
func (e *Editor) loadSourcePageContext(ctx context.Context, index int) error {
if err := ctx.Err(); err != nil {
return err
}
source := e.source.pages[e.pages[index].ID]
if source == nil || source.original != nil {
return nil
@@ -507,24 +527,24 @@ func (e *Editor) loadSourcePage(index int) error {
loaded := *source
source = &loaded
reader := e.source.reader
data, err := reader.readFile(reader.ResPath(source.ref.BaseLoc))
data, err := reader.readFileViewContext(ctx, reader.ResPath(source.ref.BaseLoc))
if err != nil {
return err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return err
}
page, err := reader.PageContent(source.ref)
page, err := reader.PageContentContext(ctx, source.ref)
if err != nil {
return err
}
data, repaired, err := e.repairSourceObjectIDs(data, root)
data, repaired, err := e.repairSourceObjectIDs(ctx, data, root)
if err != nil {
return err
}
if repaired {
root, err = parseEditorXML(data)
root, err = parseEditorXMLContext(ctx, data)
if err != nil {
return err
}
@@ -547,13 +567,17 @@ func (e *Editor) loadSourcePage(index int) error {
}
source.data, source.root, source.nodes = data, root, nodes
source.repaired = repaired
origins := make(map[string]*editorObjectOrigin)
for i := range page.Content.Layer {
layer := &page.Content.Layer[i]
for j, object := range layer.Objects {
if err := ctx.Err(); err != nil {
return err
}
id := editorObjectID(object)
if node := nodes[id]; node != nil {
origin := &editorObjectOrigin{page: source, data: data, node: node, object: object}
e.source.origins[id] = origin
origins[id] = origin
if editorXMLTransformable(node) {
resolved, err := e.resolveEditorStyle(object, layer.DrawParam)
if err != nil {
@@ -565,6 +589,10 @@ func (e *Editor) loadSourcePage(index int) error {
}
}
}
if err := ctx.Err(); err != nil {
return err
}
maps.Copy(e.source.origins, origins)
source.original = page
e.source.pages[source.ref.ID] = source
e.pages[index] = copyEditorPage(*page)