mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
ca8f22dba1
commit
d3904521b9
5 files changed
+244
-99
No files matched your search
+23
-8
@@ -1496,26 +1496,41 @@ async function exportFile(whole) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const label = whole ? (format.value === "pdf" ? "PDF" : "ZIP") : format.label;
|
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 openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
const fileName = whole ? `${baseFileName()}.${label.toLowerCase()}` : pageFileName(format.extension);
|
const fileName = whole ? `${baseFileName()}.${extension}` : pageFileName(extension);
|
||||||
|
const pageIndex = state.pageIndex;
|
||||||
|
const dpi = exportFormatUsesDPI(format.value) ? currentImageDPI() : 0;
|
||||||
state.exporting = true;
|
state.exporting = true;
|
||||||
updateControls();
|
updateControls();
|
||||||
setBusy(true, `正在生成 ${label}`, null, whole ? STATUS.exporting : STATUS.pageExporting);
|
setBusy(true, `正在生成 ${label}`, null, whole ? STATUS.exporting : STATUS.pageExporting);
|
||||||
try {
|
try {
|
||||||
const dpi = exportFormatUsesDPI(format.value) ? currentImageDPI() : 0;
|
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({
|
||||||
|
suggestedName: fileName,
|
||||||
|
types: [{ description: label, accept: { [mime]: [`.${extension}`] } }],
|
||||||
|
}) : null;
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const result = whole
|
const result = whole
|
||||||
? await callWASM("ofdgoExportDocument", format.value, dpi)
|
? await callWASM("ofdgoExportDocument", format.value, dpi, file)
|
||||||
: await callWASM("ofdgoExportPage", state.pageIndex, format.value, dpi);
|
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, file);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProgress(`正在保存 ${result.label}`, 100);
|
setProgress(`正在保存 ${result.label}`, 100);
|
||||||
const bytes = result.bytes;
|
if (result.blob) {
|
||||||
downloadBytes(bytes, result.mime, fileName);
|
downloadBytes(result.blob, result.mime, fileName);
|
||||||
setStatus(`${result.label} 导出完成 ${formatBytes(bytes.length, result.label)}`);
|
}
|
||||||
|
setStatus(`${result.label} 导出完成 ${formatBytes(result.size, result.label)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
showError(err, false);
|
if (err.name === "AbortError") {
|
||||||
|
setStatus("导出已取消");
|
||||||
|
} else {
|
||||||
|
showError(err, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
state.exporting = false;
|
state.exporting = false;
|
||||||
|
|||||||
@@ -1,14 +1,49 @@
|
|||||||
importScripts("./wasm_exec.js");
|
importScripts("./wasm_exec.js");
|
||||||
|
|
||||||
self.onmessage = ({ data: { id, name, args } }) => {
|
let pending = Promise.resolve();
|
||||||
|
self.onmessage = ({ data }) => {
|
||||||
|
pending = pending.then(() => handleMessage(data));
|
||||||
|
};
|
||||||
|
async function handleMessage({ id, name, args }) {
|
||||||
|
let output;
|
||||||
try {
|
try {
|
||||||
const payload = globalThis[name](...args);
|
const chunks = [];
|
||||||
|
let size = 0;
|
||||||
|
const exporting = name === "ofdgoExportPage" || name === "ofdgoExportDocument";
|
||||||
|
if (exporting) {
|
||||||
|
const file = args.pop();
|
||||||
|
if (file) {
|
||||||
|
output = await file.createWritable();
|
||||||
|
}
|
||||||
|
args.push((bytes, done) => {
|
||||||
|
size += bytes.length;
|
||||||
|
if (output) {
|
||||||
|
output.write(bytes).then(() => done(), (err) => done(err.message));
|
||||||
|
} else {
|
||||||
|
chunks.push(new Blob([bytes]));
|
||||||
|
setTimeout(done, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
result.data.size = size;
|
||||||
|
if (output) {
|
||||||
|
await output.close();
|
||||||
|
} else {
|
||||||
|
result.data.blob = new Blob(chunks, { type: result.data.mime });
|
||||||
|
}
|
||||||
|
} else if (output) {
|
||||||
|
await output.abort();
|
||||||
|
}
|
||||||
|
output = null;
|
||||||
self.postMessage({ id, ...result }, result.data?.bytes ? [result.data.bytes.buffer] : []);
|
self.postMessage({ id, ...result }, result.data?.bytes ? [result.data.bytes.buffer] : []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
await output?.abort().catch(() => {});
|
||||||
self.postMessage({ id, ok: false, error: err.message });
|
self.postMessage({ id, ok: false, error: err.message });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
function exitWASM(err) {
|
function exitWASM(err) {
|
||||||
self.postMessage({ type: "exit", error: err?.message || "引擎运行中断" });
|
self.postMessage({ type: "exit", error: err?.message || "引擎运行中断" });
|
||||||
|
|||||||
+77
-15
@@ -17,11 +17,43 @@
|
|||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// exportWriter 分块传递导出数据
|
||||||
|
type exportWriter struct {
|
||||||
|
write js.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write 将数据块交给浏览器保存
|
||||||
|
// 入参: data 导出数据
|
||||||
|
// 返回: int 写入长度, error 错误信息
|
||||||
|
func (w exportWriter) Write(data []byte) (int, error) {
|
||||||
|
done := make(chan error)
|
||||||
|
callback := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
var err error
|
||||||
|
if len(args) > 0 {
|
||||||
|
err = fmt.Errorf("%s", args[0].String())
|
||||||
|
}
|
||||||
|
done <- err
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer callback.Release()
|
||||||
|
size := len(data)
|
||||||
|
for len(data) > 0 {
|
||||||
|
n := min(len(data), 1<<20)
|
||||||
|
w.write.Invoke(bytesToJS(data[:n]), callback)
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
return size - len(data), err
|
||||||
|
}
|
||||||
|
data = data[n:]
|
||||||
|
}
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
// currentSession 当前WebUI文档会话
|
// currentSession 当前WebUI文档会话
|
||||||
var currentSession *Session
|
var currentSession *Session
|
||||||
|
|
||||||
@@ -42,8 +74,8 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoSVGFontData", svgFontData)
|
registerCallback("ofdgoSVGFontData", svgFontData)
|
||||||
registerCallback("ofdgoSearchPage", searchPage)
|
registerCallback("ofdgoSearchPage", searchPage)
|
||||||
registerCallback("ofdgoExportFormats", exportFormats)
|
registerCallback("ofdgoExportFormats", exportFormats)
|
||||||
registerCallback("ofdgoExportPage", exportPage)
|
registerExportCallback("ofdgoExportPage", exportPage)
|
||||||
registerCallback("ofdgoExportDocument", exportDocument)
|
registerExportCallback("ofdgoExportDocument", exportDocument)
|
||||||
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
@@ -52,18 +84,42 @@ func RunWASM() {
|
|||||||
// 入参: name 回调名称, fn 回调函数
|
// 入参: name 回调名称, fn 回调函数
|
||||||
func registerCallback(name string, fn func([]js.Value) (any, error)) {
|
func registerCallback(name string, fn func([]js.Value) (any, error)) {
|
||||||
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
|
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
data, err := safeCall(fn, args)
|
return callbackResult(fn, args)
|
||||||
if err != nil {
|
|
||||||
return encodeResult(apiResult{OK: false, Error: err.Error()})
|
|
||||||
}
|
|
||||||
if result, ok := data.(js.Value); ok {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return encodeResult(apiResult{OK: true, Data: data})
|
|
||||||
})
|
})
|
||||||
js.Global().Set(name, cb)
|
js.Global().Set(name, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registerExportCallback 异步导出,为浏览器处理数据块让出事件循环
|
||||||
|
// 入参: name 回调名称, fn 回调函数
|
||||||
|
func registerExportCallback(name string, fn func([]js.Value) (any, error)) {
|
||||||
|
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
executor := js.FuncOf(func(this js.Value, callbacks []js.Value) any {
|
||||||
|
resolve := callbacks[0]
|
||||||
|
go func() {
|
||||||
|
resolve.Invoke(callbackResult(fn, args))
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer executor.Release()
|
||||||
|
return js.Global().Get("Promise").New(executor)
|
||||||
|
})
|
||||||
|
js.Global().Set(name, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// callbackResult 转换浏览器接口结果
|
||||||
|
// 入参: fn 回调函数, args 回调参数
|
||||||
|
// 返回: any 浏览器结果
|
||||||
|
func callbackResult(fn func([]js.Value) (any, error), args []js.Value) any {
|
||||||
|
data, err := safeCall(fn, args)
|
||||||
|
if err != nil {
|
||||||
|
return encodeResult(apiResult{OK: false, Error: err.Error()})
|
||||||
|
}
|
||||||
|
if result, ok := data.(js.Value); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return encodeResult(apiResult{OK: true, Data: data})
|
||||||
|
}
|
||||||
|
|
||||||
// safeCall 调用浏览器回调并转换异常
|
// safeCall 调用浏览器回调并转换异常
|
||||||
// 入参: fn 回调函数, args 回调参数
|
// 入参: fn 回调函数, args 回调参数
|
||||||
// 返回: any 回调结果, error 错误信息
|
// 返回: any 回调结果, error 错误信息
|
||||||
@@ -238,15 +294,18 @@ func exportPage(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")
|
||||||
}
|
}
|
||||||
if len(args) < 3 {
|
if len(args) < 4 {
|
||||||
return nil, fmt.Errorf("missing export page arguments")
|
return nil, fmt.Errorf("missing export page arguments")
|
||||||
}
|
}
|
||||||
data, format, err := currentSession.ExportPage(args[0].Int(), args[1].String(), args[2].Float())
|
writer := bufio.NewWriterSize(exportWriter{write: args[3]}, 1<<20)
|
||||||
|
format, err := currentSession.ExportPage(args[0].Int(), args[1].String(), args[2].Float(), writer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := writer.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return successResult(map[string]any{
|
return successResult(map[string]any{
|
||||||
"bytes": bytesToJS(data),
|
|
||||||
"label": format.Label,
|
"label": format.Label,
|
||||||
"mime": format.MIME,
|
"mime": format.MIME,
|
||||||
}), nil
|
}), nil
|
||||||
@@ -259,12 +318,15 @@ 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, format, err := currentSession.ExportDocument(args[0].String(), args[1].Float())
|
writer := bufio.NewWriterSize(exportWriter{write: args[2]}, 1<<20)
|
||||||
|
format, err := currentSession.ExportDocument(args[0].String(), args[1].Float(), writer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := writer.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return successResult(map[string]any{
|
return successResult(map[string]any{
|
||||||
"bytes": bytesToJS(data),
|
|
||||||
"label": format.Label,
|
"label": format.Label,
|
||||||
"mime": format.MIME,
|
"mime": format.MIME,
|
||||||
}), nil
|
}), nil
|
||||||
|
|||||||
+24
-71
@@ -15,12 +15,10 @@
|
|||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/jpeg"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -450,38 +448,22 @@ func (s *Session) SVGFontData(name string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExportPage 导出单页
|
// ExportPage 导出单页
|
||||||
// 入参: index 页面索引, value 导出格式, dpi 图片DPI
|
// 入参: index 页面索引, value 导出格式, dpi 图片DPI, writer 输出流
|
||||||
// 返回: []byte 文件数据, ExportFormat 导出格式, error 错误信息
|
// 返回: ExportFormat 导出格式, error 错误信息
|
||||||
func (s *Session) ExportPage(index int, value string, dpi float64) ([]byte, ExportFormat, error) {
|
func (s *Session) ExportPage(index int, value string, dpi float64, writer io.Writer) (ExportFormat, error) {
|
||||||
format, ok := exportFormat(value)
|
format, ok := exportFormat(value)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
|
return ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
|
||||||
}
|
}
|
||||||
page, err := s.pageContent(index)
|
page, err := s.pageContent(index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ExportFormat{}, err
|
return ExportFormat{}, err
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
|
||||||
renderer := *s.Renderer
|
renderer := *s.Renderer
|
||||||
if dpi > 0 && (format.Value == "png" || format.Value == "jpg") {
|
if dpi > 0 && (format.Value == "png" || format.Value == "jpg") {
|
||||||
renderer.DPI = dpi
|
renderer.DPI = dpi
|
||||||
}
|
}
|
||||||
switch format.Value {
|
return format, renderer.RenderTo(page, writer, format.Value)
|
||||||
case "svg":
|
|
||||||
err = renderer.RenderToSVG(page, &buf)
|
|
||||||
case "pdf":
|
|
||||||
err = renderer.RenderToPDF(page, &buf)
|
|
||||||
case "eps":
|
|
||||||
err = renderer.RenderToEPS(page, &buf)
|
|
||||||
case "png":
|
|
||||||
err = renderer.RenderToPNG(page, &buf)
|
|
||||||
case "jpg":
|
|
||||||
err = renderer.RenderToJPEG(page, &buf, &jpeg.Options{Quality: 95})
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, ExportFormat{}, err
|
|
||||||
}
|
|
||||||
return buf.Bytes(), format, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// signatureInfos 获取签名验证信息
|
// signatureInfos 获取签名验证信息
|
||||||
@@ -632,72 +614,43 @@ func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExportDocument 导出文档为PDF或逐页打包ZIP
|
// ExportDocument 导出文档为PDF或逐页打包ZIP
|
||||||
// 入参: value 导出格式, dpi 图片DPI
|
// 入参: value 导出格式, dpi 图片DPI, writer 输出流
|
||||||
// 返回: []byte 文件数据, ExportFormat 导出格式, error 错误信息
|
// 返回: ExportFormat 导出格式, error 错误信息
|
||||||
func (s *Session) ExportDocument(value string, dpi float64) ([]byte, ExportFormat, error) {
|
func (s *Session) ExportDocument(value string, dpi float64, writer io.Writer) (ExportFormat, error) {
|
||||||
format, ok := exportFormat(value)
|
format, ok := exportFormat(value)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
|
return ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
|
||||||
}
|
}
|
||||||
if format.Value == "pdf" {
|
if format.Value == "pdf" {
|
||||||
data, err := s.ExportPDF()
|
return format, s.ExportPDF(writer)
|
||||||
return data, format, err
|
|
||||||
}
|
}
|
||||||
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
||||||
return nil, ExportFormat{}, fmt.Errorf("ofd document is not opened")
|
return ExportFormat{}, fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
count := len(s.doc.Pages.Page)
|
renderer := *s.Renderer
|
||||||
if count == 0 {
|
if dpi > 0 && (format.Value == "png" || format.Value == "jpg") {
|
||||||
return nil, ExportFormat{}, fmt.Errorf("no pages found")
|
renderer.DPI = dpi
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
err := renderer.RenderToZIP(writer, format.Value)
|
||||||
archive := zip.NewWriter(&buf)
|
return ExportFormat{Value: "zip", Label: "ZIP", Extension: "zip", MIME: "application/zip"}, err
|
||||||
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 错误信息
|
// 入参: writer 输出流
|
||||||
func (s *Session) ExportPDF() ([]byte, error) {
|
// 返回: error 错误信息
|
||||||
|
func (s *Session) ExportPDF(writer io.Writer) error {
|
||||||
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
||||||
return nil, fmt.Errorf("ofd document is not opened")
|
return fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
|
||||||
pages := make([]*ofdgo.PageContent, len(s.doc.Pages.Page))
|
pages := make([]*ofdgo.PageContent, len(s.doc.Pages.Page))
|
||||||
for i := range pages {
|
for i := range pages {
|
||||||
page, err := s.pageContent(i)
|
page, err := s.pageContent(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
pages[i] = page
|
pages[i] = page
|
||||||
}
|
}
|
||||||
if err := s.Renderer.RenderPagesToPDF(pages, &buf); err != nil {
|
return s.Renderer.RenderPagesToPDF(pages, writer)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pageContent 获取页面内容
|
// pageContent 获取页面内容
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -22,6 +23,8 @@ import (
|
|||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
"github.com/tdewolff/canvas/renderers/pdf"
|
"github.com/tdewolff/canvas/renderers/pdf"
|
||||||
@@ -32,6 +35,79 @@ import (
|
|||||||
"golang.org/x/image/math/f64"
|
"golang.org/x/image/math/f64"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RenderTo 按格式渲染单页,JPEG使用95画质
|
||||||
|
// 入参: page 页面内容, writer 输出流, format svg、pdf、eps、png、jpg或jpeg
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (r *Renderer) RenderTo(page *PageContent, writer io.Writer, format string) error {
|
||||||
|
_, render, err := r.outputRenderer(format)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return render(page, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputRenderer 获取格式扩展名和渲染方法
|
||||||
|
// 入参: format 输出格式
|
||||||
|
// 返回: string 扩展名, func 渲染方法, error 错误信息
|
||||||
|
func (r *Renderer) outputRenderer(format string) (string, func(*PageContent, io.Writer) error, error) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(format)) {
|
||||||
|
case "svg":
|
||||||
|
return "svg", r.RenderToSVG, nil
|
||||||
|
case "pdf":
|
||||||
|
return "pdf", r.RenderToPDF, nil
|
||||||
|
case "eps":
|
||||||
|
return "eps", r.RenderToEPS, nil
|
||||||
|
case "png":
|
||||||
|
return "png", r.RenderToPNG, nil
|
||||||
|
case "jpg", "jpeg":
|
||||||
|
return "jpg", func(page *PageContent, writer io.Writer) error {
|
||||||
|
return r.RenderToJPEG(page, writer, &jpeg.Options{Quality: 95})
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unsupported output format %s", format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出
|
||||||
|
// 入参: writer 输出流, format svg、pdf、eps、png、jpg或jpeg
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (r *Renderer) RenderToZIP(writer io.Writer, format string) error {
|
||||||
|
extension, render, err := r.outputRenderer(format)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
count, err := r.Reader.PageCount()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return fmt.Errorf("no pages found")
|
||||||
|
}
|
||||||
|
archive := zip.NewWriter(writer)
|
||||||
|
method := zip.Deflate
|
||||||
|
if extension == "png" || extension == "jpg" {
|
||||||
|
method = zip.Store
|
||||||
|
}
|
||||||
|
width := len(strconv.Itoa(count))
|
||||||
|
for index := range count {
|
||||||
|
page, err := r.Reader.PageContentByIndex(index)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read page %d: %w", index+1, err)
|
||||||
|
}
|
||||||
|
entry, err := archive.CreateHeader(&zip.FileHeader{
|
||||||
|
Name: fmt.Sprintf("%0*d.%s", width, index+1, extension),
|
||||||
|
Method: method,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := render(page, entry); err != nil {
|
||||||
|
return fmt.Errorf("failed to export page %d: %w", index+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return archive.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// rasterRenderer 保留页面物理尺寸的光栅渲染器
|
// rasterRenderer 保留页面物理尺寸的光栅渲染器
|
||||||
type rasterRenderer struct {
|
type rasterRenderer struct {
|
||||||
*rasterizer.Rasterizer
|
*rasterizer.Rasterizer
|
||||||
@@ -139,9 +215,13 @@ func (r *Renderer) RenderToSVG(page *PageContent, writer io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
renderer := svg.New(writer, c.W, c.H, nil)
|
buffer := bufio.NewWriter(writer)
|
||||||
|
renderer := svg.New(buffer, c.W, c.H, nil)
|
||||||
c.RenderTo(renderer)
|
c.RenderTo(renderer)
|
||||||
return renderer.Close()
|
if err := renderer.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return buffer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// replacePDFProducer 替换PDF的Producer属性
|
// replacePDFProducer 替换PDF的Producer属性
|
||||||
|
|||||||
Reference in new issue
Block a user