diff --git a/assets/webui/index.html b/assets/webui/index.html index 6ef4fbb..5d0100f 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -42,7 +42,7 @@
- + - +
diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 4972b4d..5462901 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -94,7 +94,7 @@ const el = { continuousButton: document.querySelector("#continuousButton"), annotationButton: document.querySelector("#annotationButton"), imageDPI: document.querySelector("#imageDPI"), - pageExportFormat: document.querySelector("#pageExportFormat"), + exportFormat: document.querySelector("#exportFormat"), exportPageButton: document.querySelector("#exportPageButton"), exportButton: document.querySelector("#exportButton"), emptyState: document.querySelector("#emptyState"), @@ -199,9 +199,9 @@ document.addEventListener("selectionchange", () => { unmountPage(index); } }); -el.pageExportFormat.addEventListener("change", () => updateDPIControl()); -el.exportPageButton.addEventListener("click", exportCurrentPage); -el.exportButton.addEventListener("click", exportPDF); +el.exportFormat.addEventListener("change", () => updateDPIControl()); +el.exportPageButton.addEventListener("click", () => exportFile(false)); +el.exportButton.addEventListener("click", () => exportFile(true)); el.refreshAppButton.addEventListener("click", refreshApplication); el.pageInput.addEventListener("change", () => { const page = Number.parseInt(el.pageInput.value, 10); @@ -593,15 +593,15 @@ async function refreshApplication() { async function loadExportFormats() { const formats = await callWASM("ofdgoExportFormats") || []; state.exportFormats = formats; - el.pageExportFormat.replaceChildren(); + el.exportFormat.replaceChildren(); for (const format of formats) { const option = document.createElement("option"); option.value = format.value; option.textContent = format.label; - el.pageExportFormat.append(option); + el.exportFormat.append(option); } if (formats.length) { - el.pageExportFormat.value = formats[0].value; + el.exportFormat.value = formats[0].value; } updateDPIControl(); } @@ -1487,61 +1487,32 @@ async function downloadAttachment(attachment) { } } -async function exportPDF() { +async function exportFile(whole) { if (!state.doc || document.body.hasAttribute("aria-busy")) { return; } - const openSeq = state.openSeq; - state.exporting = true; - updateControls(); - setBusy(true, "正在生成 PDF", 45, STATUS.exporting); - try { - const result = await callWASM("ofdgoExportPDF"); - if (openSeq !== state.openSeq) { - return; - } - setProgress("正在保存 PDF", 86); - const bytes = result.bytes; - downloadBytes(bytes, "application/pdf", pdfFileName()); - setStatus(`PDF 导出完成 ${formatBytes(result.size || bytes.length)}`); - } catch (err) { - if (openSeq === state.openSeq) { - showError(err, false); - } - } finally { - state.exporting = false; - if (openSeq === state.openSeq) { - setBusy(false); - updateControls(); - } - } -} - -async function exportCurrentPage() { - if (!state.doc || document.body.hasAttribute("aria-busy")) { - return; - } - const format = el.pageExportFormat.value; + const format = exportFormatInfo(el.exportFormat.value); if (!format) { return; } - const info = exportFormatInfo(format); - const label = info?.label || String(format || "").toUpperCase(); + const label = whole ? (format.value === "pdf" ? "PDF" : "ZIP") : format.label; const openSeq = state.openSeq; - const fileName = pageFileName(info?.extension || format); + const fileName = whole ? `${baseFileName()}.${label.toLowerCase()}` : pageFileName(format.extension); state.exporting = true; updateControls(); - setBusy(true, `正在生成 ${label}`, 45, STATUS.pageExporting); + setBusy(true, `正在生成 ${label}`, null, whole ? STATUS.exporting : STATUS.pageExporting); try { - const dpi = exportFormatUsesDPI(format) ? currentImageDPI() : 0; - const result = await callWASM("ofdgoExportPage", state.pageIndex, format, dpi); + const dpi = exportFormatUsesDPI(format.value) ? currentImageDPI() : 0; + const result = whole + ? await callWASM("ofdgoExportDocument", format.value, dpi) + : await callWASM("ofdgoExportPage", state.pageIndex, format.value, dpi); if (openSeq !== state.openSeq) { return; } - setProgress(`正在保存 ${result.label || label}`, 86); + setProgress(`正在保存 ${result.label}`, 100); const bytes = result.bytes; - downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", fileName); - setStatus(`${result.label || label} 导出完成 ${formatBytes(result.size || bytes.length, result.label || label)}`); + downloadBytes(bytes, result.mime, fileName); + setStatus(`${result.label} 导出完成 ${formatBytes(bytes.length, result.label)}`); } catch (err) { if (openSeq === state.openSeq) { showError(err, false); @@ -3224,9 +3195,9 @@ function updateControls() { el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width"); el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height"); updateAnnotationButton(); - el.pageExportFormat.disabled = !hasDoc || state.exporting || !state.exportFormats.length; + el.exportFormat.disabled = !hasDoc || state.exporting || !state.exportFormats.length; el.exportPageButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length; - el.exportButton.disabled = !hasDoc || state.exporting; + el.exportButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length; updateDPIControl(); } @@ -3329,13 +3300,9 @@ function baseFileName() { return base.replace(/[\\/:*?"<>|]+/g, "_").trim() || "ofdgo"; } -function pdfFileName() { - return `${baseFileName()}.pdf`; -} - function pageFileName(extension) { const page = String(state.pageIndex + 1).padStart(String(state.doc?.pageCount || 1).length, "0"); - return `${baseFileName()}_p${page}.${extension || "bin"}`; + return `${baseFileName()}_p${page}.${extension}`; } function exportFormatInfo(value) { @@ -3347,7 +3314,7 @@ function exportFormatUsesDPI(value) { } function updateDPIControl() { - el.imageDPI.disabled = state.exporting || !state.doc || !exportFormatUsesDPI(el.pageExportFormat.value); + el.imageDPI.disabled = state.exporting || !state.doc || !exportFormatUsesDPI(el.exportFormat.value); } function currentImageDPI() { diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index af5f4b7..34095ba 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -43,7 +43,7 @@ func RunWASM() { registerCallback("ofdgoSearchPage", searchPage) registerCallback("ofdgoExportFormats", exportFormats) registerCallback("ofdgoExportPage", exportPage) - registerCallback("ofdgoExportPDF", exportPDF) + registerCallback("ofdgoExportDocument", exportDocument) registerCallback("ofdgoMatchFontFiles", matchFontFiles) select {} } @@ -246,29 +246,27 @@ func exportPage(args []js.Value) (any, error) { return nil, err } return successResult(map[string]any{ - "bytes": bytesToJS(data), - "size": len(data), - "format": format.Value, - "label": format.Label, - "extension": format.Extension, - "mime": format.MIME, + "bytes": bytesToJS(data), + "label": format.Label, + "mime": format.MIME, }), nil } -// exportPDF 导出OFD文档为PDF +// exportDocument 导出OFD文档 // 入参: args 浏览器参数 -// 返回: any PDF结果, error 错误信息 -func exportPDF(args []js.Value) (any, error) { +// 返回: any 导出结果, error 错误信息 +func exportDocument(args []js.Value) (any, error) { if currentSession == nil { return nil, fmt.Errorf("ofd document is not opened") } - data, err := currentSession.ExportPDF() + data, format, err := currentSession.ExportDocument(args[0].String(), args[1].Float()) if err != nil { return nil, err } return successResult(map[string]any{ "bytes": bytesToJS(data), - "size": len(data), + "label": format.Label, + "mime": format.MIME, }), nil } diff --git a/internal/webui/webui.go b/internal/webui/webui.go index e99c110..679897f 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -15,10 +15,12 @@ package webui import ( + "archive/zip" "bytes" "fmt" "image/jpeg" "path" + "strconv" "strings" "time" @@ -629,6 +631,54 @@ func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureSt return infos } +// ExportDocument 导出文档为PDF或逐页打包ZIP +// 入参: value 导出格式, dpi 图片DPI +// 返回: []byte 文件数据, ExportFormat 导出格式, error 错误信息 +func (s *Session) ExportDocument(value string, dpi float64) ([]byte, ExportFormat, error) { + format, ok := exportFormat(value) + if !ok { + return nil, ExportFormat{}, fmt.Errorf("unsupported export format %s", value) + } + if format.Value == "pdf" { + data, err := s.ExportPDF() + return data, format, err + } + if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil { + return nil, ExportFormat{}, fmt.Errorf("ofd document is not opened") + } + count := len(s.doc.Pages.Page) + if count == 0 { + return nil, ExportFormat{}, fmt.Errorf("no pages found") + } + var buf bytes.Buffer + archive := zip.NewWriter(&buf) + method := zip.Deflate + if format.Value == "png" || format.Value == "jpg" { + method = zip.Store + } + width := len(strconv.Itoa(count)) + for index := range count { + data, _, err := s.ExportPage(index, format.Value, dpi) + if err != nil { + return nil, ExportFormat{}, fmt.Errorf("failed to export page %d: %w", index+1, err) + } + entry, err := archive.CreateHeader(&zip.FileHeader{ + Name: fmt.Sprintf("%0*d.%s", width, index+1, format.Extension), + Method: method, + }) + if err != nil { + return nil, ExportFormat{}, err + } + if _, err := entry.Write(data); err != nil { + return nil, ExportFormat{}, err + } + } + if err := archive.Close(); err != nil { + return nil, ExportFormat{}, err + } + return buf.Bytes(), ExportFormat{Value: "zip", Label: "ZIP", Extension: "zip", MIME: "application/zip"}, nil +} + // ExportPDF 导出文档为PDF // 返回: []byte PDF文件数据, error 错误信息 func (s *Session) ExportPDF() ([]byte, error) {