feat(文档导出): 新增多格式批量导出
build.yaml / build (push) Successful in 37s

This commit is contained in:
xiaoqidun committed 2026-09-14 09:41:42 +08:00
1 parent a29fbbd31b
commit ca8f22dba1
4 files changed
+85 -70

No files matched your search

+2 -2
View File
@@ -42,7 +42,7 @@
<button id="annotationButton" class="button" type="button" data-short="注" title="关闭注解" aria-label="关闭注解" aria-pressed="true">注解</button> <button id="annotationButton" class="button" type="button" data-short="注" title="关闭注解" aria-label="关闭注解" aria-pressed="true">注解</button>
</div> </div>
<div class="tool-group"> <div class="tool-group">
<select id="pageExportFormat" class="format-select" title="单页格式" aria-label="单页格式" disabled></select> <select id="exportFormat" class="format-select" title="导出格式" aria-label="导出格式" disabled></select>
<select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled> <select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled>
<option value="96">96</option> <option value="96">96</option>
<option value="150">150</option> <option value="150">150</option>
@@ -50,7 +50,7 @@
<option value="600">600</option> <option value="600">600</option>
</select> </select>
<button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button> <button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button>
<button id="exportButton" class="button" type="button" data-short="全" title="导出文档 PDF" aria-label="导出文档 PDF" disabled>文档</button> <button id="exportButton" class="button" type="button" data-short="全" title="导出文档" aria-label="导出文档" disabled>文档</button>
</div> </div>
</header> </header>
+23 -56
View File
@@ -94,7 +94,7 @@ const el = {
continuousButton: document.querySelector("#continuousButton"), continuousButton: document.querySelector("#continuousButton"),
annotationButton: document.querySelector("#annotationButton"), annotationButton: document.querySelector("#annotationButton"),
imageDPI: document.querySelector("#imageDPI"), imageDPI: document.querySelector("#imageDPI"),
pageExportFormat: document.querySelector("#pageExportFormat"), exportFormat: document.querySelector("#exportFormat"),
exportPageButton: document.querySelector("#exportPageButton"), exportPageButton: document.querySelector("#exportPageButton"),
exportButton: document.querySelector("#exportButton"), exportButton: document.querySelector("#exportButton"),
emptyState: document.querySelector("#emptyState"), emptyState: document.querySelector("#emptyState"),
@@ -199,9 +199,9 @@ document.addEventListener("selectionchange", () => {
unmountPage(index); unmountPage(index);
} }
}); });
el.pageExportFormat.addEventListener("change", () => updateDPIControl()); el.exportFormat.addEventListener("change", () => updateDPIControl());
el.exportPageButton.addEventListener("click", exportCurrentPage); el.exportPageButton.addEventListener("click", () => exportFile(false));
el.exportButton.addEventListener("click", exportPDF); el.exportButton.addEventListener("click", () => exportFile(true));
el.refreshAppButton.addEventListener("click", refreshApplication); el.refreshAppButton.addEventListener("click", refreshApplication);
el.pageInput.addEventListener("change", () => { el.pageInput.addEventListener("change", () => {
const page = Number.parseInt(el.pageInput.value, 10); const page = Number.parseInt(el.pageInput.value, 10);
@@ -593,15 +593,15 @@ async function refreshApplication() {
async function loadExportFormats() { async function loadExportFormats() {
const formats = await callWASM("ofdgoExportFormats") || []; const formats = await callWASM("ofdgoExportFormats") || [];
state.exportFormats = formats; state.exportFormats = formats;
el.pageExportFormat.replaceChildren(); el.exportFormat.replaceChildren();
for (const format of formats) { for (const format of formats) {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = format.value; option.value = format.value;
option.textContent = format.label; option.textContent = format.label;
el.pageExportFormat.append(option); el.exportFormat.append(option);
} }
if (formats.length) { if (formats.length) {
el.pageExportFormat.value = formats[0].value; el.exportFormat.value = formats[0].value;
} }
updateDPIControl(); updateDPIControl();
} }
@@ -1487,61 +1487,32 @@ async function downloadAttachment(attachment) {
} }
} }
async function exportPDF() { async function exportFile(whole) {
if (!state.doc || document.body.hasAttribute("aria-busy")) { if (!state.doc || document.body.hasAttribute("aria-busy")) {
return; return;
} }
const openSeq = state.openSeq; const format = exportFormatInfo(el.exportFormat.value);
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;
if (!format) { if (!format) {
return; return;
} }
const info = exportFormatInfo(format); const label = whole ? (format.value === "pdf" ? "PDF" : "ZIP") : format.label;
const label = info?.label || String(format || "").toUpperCase();
const openSeq = state.openSeq; const openSeq = state.openSeq;
const fileName = pageFileName(info?.extension || format); const fileName = whole ? `${baseFileName()}.${label.toLowerCase()}` : pageFileName(format.extension);
state.exporting = true; state.exporting = true;
updateControls(); updateControls();
setBusy(true, `正在生成 ${label}`, 45, STATUS.pageExporting); setBusy(true, `正在生成 ${label}`, null, whole ? STATUS.exporting : STATUS.pageExporting);
try { try {
const dpi = exportFormatUsesDPI(format) ? currentImageDPI() : 0; const dpi = exportFormatUsesDPI(format.value) ? currentImageDPI() : 0;
const result = await callWASM("ofdgoExportPage", state.pageIndex, format, dpi); const result = whole
? await callWASM("ofdgoExportDocument", format.value, dpi)
: await callWASM("ofdgoExportPage", state.pageIndex, format.value, dpi);
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
setProgress(`正在保存 ${result.label || label}`, 86); setProgress(`正在保存 ${result.label}`, 100);
const bytes = result.bytes; const bytes = result.bytes;
downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", fileName); downloadBytes(bytes, result.mime, fileName);
setStatus(`${result.label || label} 导出完成 ${formatBytes(result.size || bytes.length, result.label || label)}`); setStatus(`${result.label} 导出完成 ${formatBytes(bytes.length, result.label)}`);
} catch (err) { } catch (err) {
if (openSeq === state.openSeq) { if (openSeq === state.openSeq) {
showError(err, false); showError(err, false);
@@ -3224,9 +3195,9 @@ function updateControls() {
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width"); el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height"); el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
updateAnnotationButton(); 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.exportPageButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
el.exportButton.disabled = !hasDoc || state.exporting; el.exportButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
updateDPIControl(); updateDPIControl();
} }
@@ -3329,13 +3300,9 @@ function baseFileName() {
return base.replace(/[\\/:*?"<>|]+/g, "_").trim() || "ofdgo"; return base.replace(/[\\/:*?"<>|]+/g, "_").trim() || "ofdgo";
} }
function pdfFileName() {
return `${baseFileName()}.pdf`;
}
function pageFileName(extension) { function pageFileName(extension) {
const page = String(state.pageIndex + 1).padStart(String(state.doc?.pageCount || 1).length, "0"); 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) { function exportFormatInfo(value) {
@@ -3347,7 +3314,7 @@ function exportFormatUsesDPI(value) {
} }
function updateDPIControl() { 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() { function currentImageDPI() {
+10 -12
View File
@@ -43,7 +43,7 @@ func RunWASM() {
registerCallback("ofdgoSearchPage", searchPage) registerCallback("ofdgoSearchPage", searchPage)
registerCallback("ofdgoExportFormats", exportFormats) registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoExportPage", exportPage) registerCallback("ofdgoExportPage", exportPage)
registerCallback("ofdgoExportPDF", exportPDF) registerCallback("ofdgoExportDocument", exportDocument)
registerCallback("ofdgoMatchFontFiles", matchFontFiles) registerCallback("ofdgoMatchFontFiles", matchFontFiles)
select {} select {}
} }
@@ -246,29 +246,27 @@ func exportPage(args []js.Value) (any, error) {
return nil, err return nil, err
} }
return successResult(map[string]any{ return successResult(map[string]any{
"bytes": bytesToJS(data), "bytes": bytesToJS(data),
"size": len(data), "label": format.Label,
"format": format.Value, "mime": format.MIME,
"label": format.Label,
"extension": format.Extension,
"mime": format.MIME,
}), nil }), nil
} }
// exportPDF 导出OFD文档为PDF // exportDocument 导出OFD文档
// 入参: args 浏览器参数 // 入参: args 浏览器参数
// 返回: any PDF结果, error 错误信息 // 返回: any 导出结果, error 错误信息
func exportPDF(args []js.Value) (any, error) { 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")
} }
data, err := currentSession.ExportPDF() data, format, err := currentSession.ExportDocument(args[0].String(), args[1].Float())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return successResult(map[string]any{ return successResult(map[string]any{
"bytes": bytesToJS(data), "bytes": bytesToJS(data),
"size": len(data), "label": format.Label,
"mime": format.MIME,
}), nil }), nil
} }
+50
View File
@@ -15,10 +15,12 @@
package webui package webui
import ( import (
"archive/zip"
"bytes" "bytes"
"fmt" "fmt"
"image/jpeg" "image/jpeg"
"path" "path"
"strconv"
"strings" "strings"
"time" "time"
@@ -629,6 +631,54 @@ func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureSt
return infos 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 // ExportPDF 导出文档为PDF
// 返回: []byte PDF文件数据, error 错误信息 // 返回: []byte PDF文件数据, error 错误信息
func (s *Session) ExportPDF() ([]byte, error) { func (s *Session) ExportPDF() ([]byte, error) {