feat(文档导出): 新增文档导出进度显示
build.yaml / build (push) Successful in 34s

This commit is contained in:
xiaoqidun committed 2026-09-14 13:16:07 +08:00
1 parent a7e86a322f
commit 92585edd1e
6 files changed
+46 -8

No files matched your search

+1
View File
@@ -839,6 +839,7 @@ button.outline-link:hover {
overflow: hidden; overflow: hidden;
color: #1f2428; color: #1f2428;
font-size: 13px; font-size: 13px;
font-variant-numeric: tabular-nums;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
+11 -2
View File
@@ -670,6 +670,16 @@ async function loadWASM() {
} }
if (data.type === "progress") { if (data.type === "progress") {
setProgress(data.text, data.percent); setProgress(data.text, data.percent);
} else if (data.type === "export") {
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
if (data.stage === "save") {
setProgress("正在保存", null);
} else if (data.completed === data.total) {
setProgress("正在封装", null);
} else {
setProgress(`正在导出 ${data.completed} / ${data.total} 页`, data.completed / data.total * 100);
}
}
} else if (data.type === "ready") { } else if (data.type === "ready") {
state.ready = true; state.ready = true;
resolve(); resolve();
@@ -1589,7 +1599,6 @@ async function exportFile(whole, indices = null) {
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
setProgress(`正在保存 ${result.label}`, 100);
if (result.blob) { if (result.blob) {
downloadBytes(result.blob, result.mime, fileName); downloadBytes(result.blob, result.mime, fileName);
} }
@@ -3302,7 +3311,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 }); wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
try { try {
wasmWorker.postMessage({ id, name, args }); wasmWorker.postMessage({ id, name, args });
} catch (err) { } catch (err) {
+6
View File
@@ -24,11 +24,17 @@ async function handleMessage({ id, name, args }) {
setTimeout(done, 0); setTimeout(done, 0);
} }
}); });
if (name === "ofdgoExportDocument") {
args.push((completed, total) => {
self.postMessage({ id, type: "export", stage: "pages", completed, total });
});
}
} }
const payload = await globalThis[name](...args); const payload = await globalThis[name](...args);
const result = typeof payload === "string" ? JSON.parse(payload) : payload; const result = typeof payload === "string" ? JSON.parse(payload) : payload;
if (exporting && result.ok) { if (exporting && result.ok) {
result.data.size = size; result.data.size = size;
self.postMessage({ id, type: "export", stage: "save" });
if (output) { if (output) {
await output.close(); await output.close();
} else { } else {
+5
View File
@@ -335,6 +335,11 @@ func exportDocument(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")
} }
renderer := currentSession.Renderer
renderer.OnExportProgress = func(completed, total int) {
args[4].Invoke(completed, total)
}
defer func() { renderer.OnExportProgress = nil }()
var indices []int var indices []int
if !args[2].IsNull() { if !args[2].IsNull() {
indices = make([]int, args[2].Length()) indices = make([]int, args[2].Length())
+2
View File
@@ -24,11 +24,13 @@ import (
// Renderer 渲染器实现 // Renderer 渲染器实现
// 通过NewRenderer创建,实例及共享的Reader需串行使用 // 通过NewRenderer创建,实例及共享的Reader需串行使用
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观 // OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,不代表封装或写入成功
type Renderer struct { type Renderer struct {
Reader *Reader Reader *Reader
DPI float64 DPI float64
RenderAnnotations bool RenderAnnotations bool
OnPageText func(*PageContent, *PageText) OnPageText func(*PageContent, *PageText)
OnExportProgress func(completed, total int)
DrawParams map[string]*DrawParam DrawParams map[string]*DrawParam
CompositeGraphicUnits map[string]*CompositeGraphicUnit CompositeGraphicUnits map[string]*CompositeGraphicUnit
fontFamily *canvas.FontFamily fontFamily *canvas.FontFamily
+21 -6
View File
@@ -90,7 +90,10 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int)
method = zip.Store method = zip.Store
} }
width := len(strconv.Itoa(count)) width := len(strconv.Itoa(count))
for _, index := range indices { if r.OnExportProgress != nil {
r.OnExportProgress(0, len(indices))
}
for i, index := range indices {
page, err := r.Reader.PageContentByIndex(index) page, err := r.Reader.PageContentByIndex(index)
if err != nil { if err != nil {
return fmt.Errorf("failed to read page %d: %w", index+1, err) return fmt.Errorf("failed to read page %d: %w", index+1, err)
@@ -105,6 +108,9 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int)
if err := render(page, entry); err != nil { if err := render(page, entry); err != nil {
return fmt.Errorf("failed to export page %d: %w", index+1, err) return fmt.Errorf("failed to export page %d: %w", index+1, err)
} }
if r.OnExportProgress != nil {
r.OnExportProgress(i+1, len(indices))
}
} }
return archive.Close() return archive.Close()
} }
@@ -248,7 +254,7 @@ func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
return err return err
} }
pages := []pdfPage{{Content: page, Box: box}} pages := []pdfPage{{Content: page, Box: box}}
return r.renderPDFPages(pages, writer) return r.renderPDFPages(pages, writer, nil)
} }
// RenderToEPS 渲染为EPS // RenderToEPS 渲染为EPS
@@ -282,6 +288,9 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error
if err != nil { if err != nil {
return err return err
} }
if r.OnExportProgress != nil {
r.OnExportProgress(0, len(indices))
}
pages := make([]pdfPage, len(indices)) pages := make([]pdfPage, len(indices))
for i, index := range indices { for i, index := range indices {
page, err := r.Reader.PageContent(doc.Pages.Page[index]) page, err := r.Reader.PageContent(doc.Pages.Page[index])
@@ -294,7 +303,7 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error
} }
pages[i] = pdfPage{Content: page, Box: box} pages[i] = pdfPage{Content: page, Box: box}
} }
return r.renderPDFPages(pages, writer) return r.renderPDFPages(pages, writer, r.OnExportProgress)
} }
// RenderPagesToPDF 按指定顺序导出已解析的页面,复用调用方的页面缓存 // RenderPagesToPDF 按指定顺序导出已解析的页面,复用调用方的页面缓存
@@ -304,6 +313,9 @@ func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) e
if len(contents) == 0 { if len(contents) == 0 {
return fmt.Errorf("no pages found") return fmt.Errorf("no pages found")
} }
if r.OnExportProgress != nil {
r.OnExportProgress(0, len(contents))
}
pages := make([]pdfPage, len(contents)) pages := make([]pdfPage, len(contents))
for i, page := range contents { for i, page := range contents {
box, err := r.GetPageBox(page) box, err := r.GetPageBox(page)
@@ -312,13 +324,13 @@ func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) e
} }
pages[i] = pdfPage{Content: page, Box: box} pages[i] = pdfPage{Content: page, Box: box}
} }
return r.renderPDFPages(pages, writer) return r.renderPDFPages(pages, writer, r.OnExportProgress)
} }
// renderPDFPages 渲染页面并复用调用方的字节缓冲 // renderPDFPages 渲染页面并复用调用方的字节缓冲
// 入参: pages 页面列表, writer 输出流 // 入参: pages 页面列表, writer 输出流, progress 页面处理进度
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer) error { func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer, progress func(int, int)) error {
navigation := newPDFNavigation(r, r.Reader.doc, pages) navigation := newPDFNavigation(r, r.Reader.doc, pages)
buf, direct := writer.(*bytes.Buffer) buf, direct := writer.(*bytes.Buffer)
if !direct { if !direct {
@@ -337,6 +349,9 @@ func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer) error {
buf.Truncate(start) buf.Truncate(start)
return fmt.Errorf("failed to render page %d: %w", i+1, err) return fmt.Errorf("failed to render page %d: %w", i+1, err)
} }
if progress != nil {
progress(i+1, len(pages))
}
} }
if err := p.Close(); err != nil { if err := p.Close(); err != nil {
buf.Truncate(start) buf.Truncate(start)