feat(内存占用): 支持附件流式下载,降低大附件内存占用
build.yaml / build (push) Successful in 37s

This commit is contained in:
xiaoqidun committed 2026-09-14 16:15:47 +08:00
1 parent fb3a9e8148
commit 6525b01bab
4 files changed
+49 -14

No files matched your search

+16 -4
View File
@@ -1535,18 +1535,30 @@ async function downloadAttachment(attachment) {
updateControls(); updateControls();
setBusy(true, "正在读取附件", null, "正在读取附件"); setBusy(true, "正在读取附件", null, "正在读取附件");
try { try {
const result = await callWASM("ofdgoAttachmentData", attachment.id); const file = window.showSaveFilePicker ? await window.showSaveFilePicker({ suggestedName: attachment.fileName }) : null;
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
downloadBytes(result.bytes, "application/octet-stream", attachment.fileName); const result = await callWASM("ofdgoExportAttachment", attachment.id, file);
setStatus(`附件下载完成 ${formatBytes(result.bytes.length)}`); if (openSeq !== state.openSeq) {
return;
}
if (result.blob) {
downloadBytes(result.blob, result.mime, attachment.fileName);
}
setStatus(`附件下载完成 ${formatBytes(result.size)}`);
} catch (err) { } catch (err) {
if (openSeq === state.openSeq) { if (openSeq === state.openSeq) {
if (err.name === "AbortError") {
setStatus("下载已取消");
} else {
showError(err, false); showError(err, false);
} }
}
} finally { } finally {
state.exporting = false; state.exporting = false;
state.exportRequestID = 0;
el.cancelExportButton.hidden = true;
if (openSeq === state.openSeq) { if (openSeq === state.openSeq) {
setBusy(false); setBusy(false);
updateControls(); updateControls();
@@ -3416,7 +3428,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") { if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment") {
state.exportRequestID = id; state.exportRequestID = id;
el.cancelExportButton.hidden = false; el.cancelExportButton.hidden = false;
el.cancelExportButton.disabled = false; el.cancelExportButton.disabled = false;
+1 -1
View File
@@ -7,7 +7,7 @@ self.onmessage = ({ data }) => {
exports.get(data.id)?.abort(); exports.get(data.id)?.abort();
return; return;
} }
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument") { if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment") {
exports.set(data.id, new AbortController()); exports.set(data.id, new AbortController());
} }
pending = pending.then(() => handleMessage(data)); pending = pending.then(() => handleMessage(data));
+16 -5
View File
@@ -21,6 +21,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"syscall/js" "syscall/js"
"github.com/xiaoqidun/ofdgo" "github.com/xiaoqidun/ofdgo"
@@ -81,7 +82,6 @@ func RunWASM() {
registerCallback("ofdgoOpen", openDocument) registerCallback("ofdgoOpen", openDocument)
registerCallback("ofdgoConfigure", configureDocument) registerCallback("ofdgoConfigure", configureDocument)
registerCallback("ofdgoDocumentInfo", documentInfo) registerCallback("ofdgoDocumentInfo", documentInfo)
registerCallback("ofdgoAttachmentData", attachmentData)
registerCallback("ofdgoRenderPage", renderPage) registerCallback("ofdgoRenderPage", renderPage)
registerCallback("ofdgoSVGFontData", svgFontData) registerCallback("ofdgoSVGFontData", svgFontData)
registerCallback("ofdgoSearchPage", searchPage) registerCallback("ofdgoSearchPage", searchPage)
@@ -90,6 +90,7 @@ func RunWASM() {
registerCallback("ofdgoParsePageRange", parsePageRange) registerCallback("ofdgoParsePageRange", parsePageRange)
registerExportCallback("ofdgoExportPage", exportPage) registerExportCallback("ofdgoExportPage", exportPage)
registerExportCallback("ofdgoExportDocument", exportDocument) registerExportCallback("ofdgoExportDocument", exportDocument)
registerExportCallback("ofdgoExportAttachment", exportAttachment)
registerCallback("ofdgoMatchFontFiles", matchFontFiles) registerCallback("ofdgoMatchFontFiles", matchFontFiles)
select {} select {}
} }
@@ -207,18 +208,28 @@ func documentInfo(args []js.Value) (any, error) {
return currentSession.Info(), nil return currentSession.Info(), nil
} }
// attachmentData 读取附件数据 // exportAttachment 分块导出附件
// 入参: args 浏览器参数 // 入参: args 浏览器参数
// 返回: any 附件数据, error 错误信息 // 返回: any 附件数据, error 错误信息
func attachmentData(args []js.Value) (any, error) { func exportAttachment(args []js.Value) (any, error) {
if currentSession == nil { if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened") 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 { if err != nil {
return nil, err 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字体资源 // svgFontData 读取SVG字体资源
+16 -4
View File
@@ -543,22 +543,34 @@ func (r *Reader) Attachments() ([]Attachment, error) {
return doc.Attachments.Attachment, nil return doc.Attachments.Attachment, nil
} }
// AttachmentData 获取附件文件数据 // OpenAttachment 打开附件文件流,调用方负责关闭,完整读取后校验ZIP数据
// 入参: id 附件标识 // 入参: id 附件标识
// 返回: []byte 附件数据, error 错误信息 // 返回: io.ReadCloser 附件文件流, error 错误信息
func (r *Reader) AttachmentData(id string) ([]byte, error) { func (r *Reader) OpenAttachment(id string) (io.ReadCloser, error) {
attachments, err := r.Attachments() attachments, err := r.Attachments()
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, attachment := range attachments { for _, attachment := range attachments {
if attachment.ID == id { 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) 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 获取自定义标引 // CustomTags 获取自定义标引
// 返回: []CustomTag 自定义标引列表, error 错误信息 // 返回: []CustomTag 自定义标引列表, error 错误信息
func (r *Reader) CustomTags() ([]CustomTag, error) { func (r *Reader) CustomTags() ([]CustomTag, error) {