diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 7c7cdc3..914b3fc 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -695,7 +695,7 @@ async function loadWASM() { setProgress("正在保存", null); } else if (!el.cancelExportButton.disabled) { if (data.completed === data.total) { - setProgress("正在封装", null); + setProgress("正在收尾", null); } else { setProgress(`正在导出 ${data.completed} / ${data.total} 页`, data.completed / data.total * 100); } @@ -1540,7 +1540,7 @@ async function downloadAttachment(attachment) { return; } downloadBytes(result.bytes, "application/octet-stream", attachment.fileName); - setStatus(`附件下载完成 ${formatBytes(result.bytes.length, "0 KB")}`); + setStatus(`附件下载完成 ${formatBytes(result.bytes.length)}`); } catch (err) { if (openSeq === state.openSeq) { showError(err, false); @@ -1600,9 +1600,10 @@ async function exportFile(whole, indices = null) { if (!format) { return; } - const label = whole ? (format.value === "pdf" ? "PDF" : "ZIP") : format.label; - const extension = whole ? label.toLowerCase() : format.extension; - const mime = whole && format.value !== "pdf" ? "application/zip" : format.mime; + const archive = whole && format.value !== "pdf" && format.value !== "txt"; + const label = archive ? "ZIP" : format.label; + const extension = archive ? "zip" : format.extension; + const mime = archive ? "application/zip" : format.mime; const openSeq = state.openSeq; const fileName = whole ? `${baseFileName()}.${extension}` : pageFileName(extension); const pageIndex = state.pageIndex; @@ -1627,7 +1628,7 @@ async function exportFile(whole, indices = null) { if (result.blob) { downloadBytes(result.blob, result.mime, fileName); } - setStatus(`${result.label} 导出完成 ${formatBytes(result.size, result.label)}`); + setStatus(`${result.label} 导出完成 ${formatBytes(result.size)}`); } catch (err) { if (openSeq === state.openSeq) { if (err.name === "AbortError") { @@ -2737,7 +2738,7 @@ function renderAttachments() { name.className = "attachment-name"; name.textContent = attachment.name; item.append(name); - const detail = [attachment.format, attachment.size == null ? "" : formatBytes(attachment.size * 1024, "0 KB")].filter(Boolean).join(" · "); + const detail = [attachment.format, attachment.size == null ? "" : formatBytes(attachment.size * 1024)].filter(Boolean).join(" · "); if (detail) { const meta = document.createElement("span"); meta.className = "attachment-detail"; @@ -3524,9 +3525,9 @@ function currentImageDPI() { return Number.parseFloat(el.imageDPI.value) || DEFAULT_IMAGE_DPI; } -function formatBytes(size, fallback = "PDF") { +function formatBytes(size) { if (!Number.isFinite(size) || size <= 0) { - return fallback; + return "0 KB"; } if (size < 1024 * 1024) { return `${Math.round(size / 1024)} KB`; diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 008cc21..4f12bd1 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -179,7 +179,8 @@ var supportedExportFormats = []ExportFormat{ {Value: "pdf", Label: "PDF", Extension: "pdf", MIME: "application/pdf"}, {Value: "eps", Label: "EPS", Extension: "eps", MIME: "application/postscript"}, {Value: "png", Label: "PNG", Extension: "png", MIME: "image/png"}, - {Value: "jpg", Label: "JPEG", Extension: "jpg", MIME: "image/jpeg"}, + {Value: "jpg", Label: "JPG", Extension: "jpg", MIME: "image/jpeg"}, + {Value: "txt", Label: "TXT", Extension: "txt", MIME: "text/plain"}, } // ExportFormats 获取导出格式 @@ -631,7 +632,7 @@ func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureSt return infos } -// ExportDocument 导出文档为PDF或逐页打包ZIP +// ExportDocument 导出文档为PDF、TXT或逐页打包ZIP // 入参: value 导出格式, dpi 图片DPI, writer 输出流, indices 零基页面索引,省略则全部 // 返回: ExportFormat 导出格式, error 错误信息 func (s *Session) ExportDocument(value string, dpi float64, writer io.Writer, indices ...int) (ExportFormat, error) { @@ -645,6 +646,9 @@ func (s *Session) ExportDocument(value string, dpi float64, writer io.Writer, in if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil { return ExportFormat{}, fmt.Errorf("ofd document is not opened") } + if format.Value == "txt" { + return format, s.Renderer.RenderToMultiPageText(writer, indices...) + } renderer := *s.Renderer if dpi > 0 && (format.Value == "png" || format.Value == "jpg") { renderer.DPI = dpi diff --git a/ofdgo_renderer_output.go b/ofdgo_renderer_output.go index 11fbf85..547f297 100644 --- a/ofdgo_renderer_output.go +++ b/ofdgo_renderer_output.go @@ -36,7 +36,7 @@ import ( ) // RenderTo 按格式渲染单页,JPEG使用95画质 -// 入参: page 页面内容, writer 输出流, format svg、pdf、eps、png、jpg或jpeg +// 入参: page 页面内容, writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt // 返回: error 错误信息 func (r *Renderer) RenderTo(page *PageContent, writer io.Writer, format string) error { _, render, err := r.outputRenderer(format) @@ -55,6 +55,8 @@ func (r *Renderer) outputRenderer(format string) (string, func(*PageContent, io. return "svg", r.RenderToSVG, nil case "pdf": return "pdf", r.RenderToPDF, nil + case "txt": + return "txt", r.RenderToText, nil case "eps": return "eps", r.RenderToEPS, nil case "png": @@ -69,7 +71,7 @@ func (r *Renderer) outputRenderer(format string) (string, func(*PageContent, io. } // RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出 -// 入参: writer 输出流, format svg、pdf、eps、png、jpg或jpeg, indices 零基页面索引,省略则全部,按原页序去重 +// 入参: writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt, indices 零基页面索引,省略则全部,按原页序去重 // 返回: error 错误信息 func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int) error { extension, render, err := r.outputRenderer(format) @@ -115,6 +117,61 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int) return archive.Close() } +// RenderToText 导出页面原文为UTF-8文本,以换行分隔非空文本对象 +// 入参: page 页面内容, writer 输出流 +// 返回: error 错误信息 +func (r *Renderer) RenderToText(page *PageContent, writer io.Writer) error { + text, err := r.PageText(page) + if err != nil { + return err + } + _, err = io.WriteString(writer, text.String()) + return err +} + +// RenderToMultiPageText 逐页导出UTF-8原文,以换行分隔非空页面,出错时调用方应丢弃输出 +// 入参: writer 输出流, indices 零基页面索引,省略则全部,按原页序去重 +// 返回: error 错误信息 +func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error { + count, err := r.Reader.PageCount() + if err != nil { + return err + } + indices, err = exportPageIndices(count, indices) + if err != nil { + return err + } + if err := r.exportProgress(0, len(indices)); err != nil { + return err + } + written := false + for i, index := range indices { + page, err := r.Reader.PageContentByIndex(index) + if err != nil { + return fmt.Errorf("failed to read page %d: %w", index+1, err) + } + text, err := r.PageText(page) + if err != nil { + return fmt.Errorf("failed to extract page %d text: %w", index+1, err) + } + if value := text.String(); value != "" { + if written { + if _, err := io.WriteString(writer, "\n"); err != nil { + return err + } + } + if _, err := io.WriteString(writer, value); err != nil { + return err + } + written = true + } + if err := r.exportProgress(i+1, len(indices)); err != nil { + return err + } + } + return nil +} + // exportProgress 回报导出进度并传递调用方的停止原因 // 入参: completed 已处理页数, total 总页数 // 返回: error 停止原因