diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 7fc881d..aa94c9c 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -3215,6 +3215,14 @@ async function loadWASM() { } if (data.type === "progress") { setProgress(data.text, data.percent); + } else if (data.type === "conversion") { + if (wasmRequests.get(data.id)?.openSeq === state.openSeq && !el.cancelExportButton.disabled) { + if (data.phase === "open") setProgress("正在读取 PDF", 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 if (data.phase === "write.fonts") setProgress("正在处理字体", null); + else setProgress("正在生成 OFD", null); + } } else if (data.type === "export") { if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) { if (data.stage === "save") { @@ -3344,7 +3352,15 @@ async function openOFD(file) { setProgress("正在转换 PDF", 25); await ensureWASM(); if (openSeq !== state.openSeq) return; - const converted = await callWASM("ofdgoConvertPDF", bytes); + let converted; + try { + converted = await callWASM("ofdgoConvertPDF", bytes); + } finally { + if (openSeq === state.openSeq) { + state.exportRequestID = 0; + el.cancelExportButton.hidden = true; + } + } if (openSeq !== state.openSeq) return; bytes = converted.bytes; warnings = JSON.parse(converted.warnings || "null") || []; @@ -3384,7 +3400,8 @@ async function openOFD(file) { await openDocument({ pageIndex: 0, resetScroll: true, openSeq }); } catch (err) { if (openSeq === state.openSeq) { - showError(err, !state.doc); + if (err.name === "AbortError") setStatus("转换已取消"); + else showError(err, !state.doc); setBusy(false); } } @@ -7299,11 +7316,11 @@ async function callWASM(name, ...args) { return new Promise((resolve, reject) => { const id = ++wasmRequestID; wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq }); - if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") { + if (name === "ofdgoConvertPDF" || name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") { state.exportRequestID = id; el.cancelExportButton.hidden = false; el.cancelExportButton.disabled = false; - el.cancelExportButton.title = { ofdgoSaveDocument: "取消保存", ofdgoSaveEncrypted: "取消保存", ofdgoSaveSigned: "取消签署", ofdgoImportPages: "取消导入", ofdgoExportAttachment: "取消下载" }[name] || "取消导出"; + el.cancelExportButton.title = { ofdgoConvertPDF: "取消转换", ofdgoSaveDocument: "取消保存", ofdgoSaveEncrypted: "取消保存", ofdgoSaveSigned: "取消签署", ofdgoImportPages: "取消导入", ofdgoExportAttachment: "取消下载" }[name] || "取消导出"; el.cancelExportButton.setAttribute("aria-label", el.cancelExportButton.title); } try { diff --git a/assets/webui/ofdgo_wasm.js b/assets/webui/ofdgo_wasm.js index 9c0387d..4f23bf4 100644 --- a/assets/webui/ofdgo_wasm.js +++ b/assets/webui/ofdgo_wasm.js @@ -7,7 +7,7 @@ self.onmessage = ({ data }) => { operations.get(data.id)?.abort(); return; } - if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveEncrypted" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") { + if (data.name === "ofdgoConvertPDF" || data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveEncrypted" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") { operations.set(data.id, new AbortController()); } pending = pending.then(() => handleMessage(data)); @@ -17,6 +17,7 @@ async function handleMessage({ id, name, args }) { let channel; const signal = operations.get(id)?.signal; const importing = name === "ofdgoImportPages"; + const converting = name === "ofdgoConvertPDF"; const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned"; 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; @@ -34,10 +35,10 @@ async function handleMessage({ id, name, args }) { }; channel.port2.postMessage(null); }; - if (importing) { + if (importing || converting) { args.push((phase, completed, total, done) => { - self.postMessage({ id, type: "import", phase, completed, total }); - finish(done, null, phase === "commit"); + self.postMessage({ id, type: importing ? "import" : "conversion", phase, completed, total }); + finish(done, null, importing && phase === "commit"); }); } else { const file = args.pop(); @@ -74,7 +75,7 @@ async function handleMessage({ id, name, args }) { const payload = await globalThis[name](...args); signal?.throwIfAborted(); const result = typeof payload === "string" ? JSON.parse(payload) : payload; - if (signal && !importing && result.ok) { + if (signal && !importing && !converting && result.ok) { operations.delete(id); result.data.size = size; self.postMessage({ id, type: "export", stage: "save" }); @@ -93,7 +94,7 @@ async function handleMessage({ id, name, args }) { await output?.abort().catch(() => {}); const result = { id, ok: false, error: err.message }; if (signal?.aborted) { - result.error = importing ? "导入已取消" : "导出已取消"; + result.error = importing ? "导入已取消" : converting ? "转换已取消" : "导出已取消"; result.canceled = true; } self.postMessage(result); diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 29014e8..118a25b 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -156,7 +156,7 @@ func awaitExport(fn js.Value, args ...any) error { // RunWASM 注册浏览器WASM接口并阻塞运行 func RunWASM() { registerCallback("ofdgoOpen", openDocument) - registerCallback("ofdgoConvertPDF", convertPDFDocument) + registerAsyncCallback("ofdgoConvertPDF", convertPDFDocument) registerCallback("ofdgoConfigure", configureDocument) registerCallback("ofdgoDocumentInfo", documentInfo) registerCallback("ofdgoVerifySignatures", verifySignatures) @@ -316,7 +316,7 @@ func safeCall(fn func([]js.Value) (any, error), args []js.Value) (data any, err // 入参: args PDF数据 // 返回: any OFD数据及转换警告, error 错误信息 func convertPDFDocument(args []js.Value) (any, error) { - if len(args) != 1 { + if len(args) != 2 { return nil, fmt.Errorf("missing PDF data") } data, err := bytesFromJS(args[0]) @@ -324,7 +324,8 @@ func convertPDFDocument(args []js.Value) (any, error) { return nil, err } var output bytes.Buffer - report, err := ofdgo.ConvertPDF(context.Background(), bytes.NewReader(data), int64(len(data)), &output, ofdgo.PDFImportOptions{}) + options := ofdgo.PDFImportOptions{OnProgress: editorOperationProgress(args[1])} + report, err := ofdgo.ConvertPDF(context.Background(), bytes.NewReader(data), int64(len(data)), &output, options) if err != nil { return nil, err } diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 756f816..6c2d17b 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -28,10 +28,12 @@ import ( // PDFImportOptions 指定PDF转换的运行时后端、进度通知与检查策略 // Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告 +// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知 type PDFImportOptions struct { - Backends *RenderBackends - Progress func(int) error - Strict bool + Backends *RenderBackends + Progress func(int) error + OnProgress func(stage string, completed, total int) error + Strict bool } // PDFImportReport 汇总转换页数、对象数、链接数和转换警告 @@ -73,6 +75,11 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI if err := ctx.Err(); err != nil { return nil, PDFImportReport{}, err } + if options.OnProgress != nil { + if err := options.OnProgress("open", 0, 0); err != nil { + return nil, PDFImportReport{}, err + } + } reader, err := pdfgo.NewReader(source, size) if err != nil { return nil, PDFImportReport{}, err @@ -82,6 +89,11 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI editor.SetRenderBackends(*options.Backends) } importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}} + if options.OnProgress != nil { + if err := options.OnProgress("pages", 0, 0); err != nil { + return nil, PDFImportReport{}, err + } + } err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { _, width, height := pdfPageMatrix(page) if _, err := editor.AddPage(width, height); err != nil { @@ -89,11 +101,19 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI } importer.pages[page.Reference] = page importer.pageIDs[page.Reference] = editor.pages[index].ID + if options.OnProgress != nil { + return options.OnProgress("pages", index+1, 0) + } return nil }) if err != nil { return nil, PDFImportReport{}, err } + if options.OnProgress != nil { + if err := options.OnProgress("convert", 0, len(editor.pages)); err != nil { + return nil, PDFImportReport{}, err + } + } err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { matrix, width, height := pdfPageMatrix(page) importer.matrix = matrix @@ -125,6 +145,11 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI return fmt.Errorf("import PDF page %d: %w", index+1, err) } importer.report.Pages++ + if options.OnProgress != nil { + if err := options.OnProgress("convert", index+1, len(editor.pages)); err != nil { + return err + } + } if options.Progress != nil { return options.Progress(index + 1) } @@ -163,6 +188,14 @@ func ConvertPDF(ctx context.Context, source io.ReaderAt, size int64, output io.W if err := ctx.Err(); err != nil { return PDFImportReport{}, err } + if options.OnProgress != nil { + if err := options.OnProgress("write", 0, 0); err != nil { + return PDFImportReport{}, err + } + editor.OnWriteProgress = func(stage string, completed, total int) error { + return options.OnProgress("write."+stage, completed, total) + } + } _, err = editor.WriteTo(output) return report, err }