From 6525b01bab1103a3e149b0611923d23d9251e81e Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 14 Sep 2026 16:15:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=86=85=E5=AD=98=E5=8D=A0=E7=94=A8):=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=99=84=E4=BB=B6=E6=B5=81=E5=BC=8F=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=EF=BC=8C=E9=99=8D=E4=BD=8E=E5=A4=A7=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=86=85=E5=AD=98=E5=8D=A0=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 22 +++++++++++++++++----- assets/webui/ofdgo.wasm.js | 2 +- internal/webui/wasm.go | 21 ++++++++++++++++----- ofdgo_reader.go | 20 ++++++++++++++++---- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 914b3fc..982d47c 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -1535,18 +1535,30 @@ async function downloadAttachment(attachment) { updateControls(); setBusy(true, "正在读取附件", null, "正在读取附件"); try { - const result = await callWASM("ofdgoAttachmentData", attachment.id); + const file = window.showSaveFilePicker ? await window.showSaveFilePicker({ suggestedName: attachment.fileName }) : null; if (openSeq !== state.openSeq) { return; } - downloadBytes(result.bytes, "application/octet-stream", attachment.fileName); - setStatus(`附件下载完成 ${formatBytes(result.bytes.length)}`); + const result = await callWASM("ofdgoExportAttachment", attachment.id, file); + if (openSeq !== state.openSeq) { + return; + } + if (result.blob) { + downloadBytes(result.blob, result.mime, attachment.fileName); + } + setStatus(`附件下载完成 ${formatBytes(result.size)}`); } catch (err) { if (openSeq === state.openSeq) { - showError(err, false); + if (err.name === "AbortError") { + setStatus("下载已取消"); + } else { + showError(err, false); + } } } finally { state.exporting = false; + state.exportRequestID = 0; + el.cancelExportButton.hidden = true; if (openSeq === state.openSeq) { setBusy(false); updateControls(); @@ -3416,7 +3428,7 @@ 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") { + if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment") { state.exportRequestID = id; el.cancelExportButton.hidden = false; el.cancelExportButton.disabled = false; diff --git a/assets/webui/ofdgo.wasm.js b/assets/webui/ofdgo.wasm.js index b52f90b..73818c5 100644 --- a/assets/webui/ofdgo.wasm.js +++ b/assets/webui/ofdgo.wasm.js @@ -7,7 +7,7 @@ self.onmessage = ({ data }) => { exports.get(data.id)?.abort(); return; } - if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument") { + if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment") { exports.set(data.id, new AbortController()); } pending = pending.then(() => handleMessage(data)); diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index b85d5cd..b901a3d 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -21,6 +21,7 @@ import ( "context" "encoding/json" "fmt" + "io" "syscall/js" "github.com/xiaoqidun/ofdgo" @@ -81,7 +82,6 @@ func RunWASM() { registerCallback("ofdgoOpen", openDocument) registerCallback("ofdgoConfigure", configureDocument) registerCallback("ofdgoDocumentInfo", documentInfo) - registerCallback("ofdgoAttachmentData", attachmentData) registerCallback("ofdgoRenderPage", renderPage) registerCallback("ofdgoSVGFontData", svgFontData) registerCallback("ofdgoSearchPage", searchPage) @@ -90,6 +90,7 @@ func RunWASM() { registerCallback("ofdgoParsePageRange", parsePageRange) registerExportCallback("ofdgoExportPage", exportPage) registerExportCallback("ofdgoExportDocument", exportDocument) + registerExportCallback("ofdgoExportAttachment", exportAttachment) registerCallback("ofdgoMatchFontFiles", matchFontFiles) select {} } @@ -207,18 +208,28 @@ func documentInfo(args []js.Value) (any, error) { return currentSession.Info(), nil } -// attachmentData 读取附件数据 +// exportAttachment 分块导出附件 // 入参: args 浏览器参数 // 返回: any 附件数据, error 错误信息 -func attachmentData(args []js.Value) (any, error) { +func exportAttachment(args []js.Value) (any, error) { if currentSession == nil { return nil, fmt.Errorf("ofd document is not opened") } - data, err := currentSession.Reader.AttachmentData(args[0].String()) + stream, err := currentSession.Reader.OpenAttachment(args[0].String()) if err != nil { return nil, err } - return successResult(map[string]any{"bytes": bytesToJS(data)}), nil + defer stream.Close() + writer := bufio.NewWriterSize(exportWriter{write: args[1]}, 1<<20) + if _, err := io.Copy(writer, stream); err != nil { + return nil, err + } + if err := writer.Flush(); err != nil { + return nil, err + } + return successResult(map[string]any{ + "mime": "application/octet-stream", + }), nil } // svgFontData 读取SVG字体资源 diff --git a/ofdgo_reader.go b/ofdgo_reader.go index 4bb034e..13a3b2d 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -543,22 +543,34 @@ func (r *Reader) Attachments() ([]Attachment, error) { return doc.Attachments.Attachment, nil } -// AttachmentData 获取附件文件数据 +// OpenAttachment 打开附件文件流,调用方负责关闭,完整读取后校验ZIP数据 // 入参: id 附件标识 -// 返回: []byte 附件数据, error 错误信息 -func (r *Reader) AttachmentData(id string) ([]byte, error) { +// 返回: io.ReadCloser 附件文件流, error 错误信息 +func (r *Reader) OpenAttachment(id string) (io.ReadCloser, error) { attachments, err := r.Attachments() if err != nil { return nil, err } for _, attachment := range attachments { if attachment.ID == id { - return r.ResData(attachment.FileLoc) + return r.openFile(r.ResPath(attachment.FileLoc)) } } return nil, fmt.Errorf("attachment not found: %s", id) } +// AttachmentData 获取附件文件数据 +// 入参: id 附件标识 +// 返回: []byte 附件数据, error 错误信息 +func (r *Reader) AttachmentData(id string) ([]byte, error) { + stream, err := r.OpenAttachment(id) + if err != nil { + return nil, err + } + defer stream.Close() + return io.ReadAll(stream) +} + // CustomTags 获取自定义标引 // 返回: []CustomTag 自定义标引列表, error 错误信息 func (r *Reader) CustomTags() ([]CustomTag, error) {