概述
在 Go 语言生态系统中,有多种 PDF 生成和处理方案可供选择。本文将深入比较主流 Go PDF 库,并提供实际应用场景的解决方案,帮助开发者根据项目需求做出合适的选择。
核心库对比分析
以下是各 PDF 库的功能对比表格:
特性 | go-pdf | fpdf | unidoc | pdfcpu | wkhtmltopdf | chromedp |
---|---|---|---|---|---|---|
创建 PDF | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
编辑 PDF | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
HTML 转 PDF | ❌ | ⚠️ | ❌ | ❌ | ✅ | ✅ |
表格支持 | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ |
中文字体 | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
开源协议 | MIT | AGPL | AGPL/商业 | Apache 2.0 | LGPL | MIT |
性能 | 高 | 中 | 中 | 高 | 低 | 低 |
学习曲线 | 低 | 中 | 高 | 中 | 低 | 中 |
详细库分析与代码示例
1. go-pdf - 简单轻量
package main
import (
"github.com/signintech/gopdf"
"log"
)
func main() {
pdf := gopdf.GoPdf{}
pdf.Start(gopdf.Config{Unit: "pt", PageSize: gopdf.Rect{W: 595.28, H: 841.89}})
pdf.AddPage()
// 添加中文字体支持
err := pdf.AddTTFFont("simsun", "./fonts/simsun.ttf")
if err != nil {
log.Fatal(err)
}
err = pdf.SetFont("simsun", "", 14)
if err != nil {
log.Fatal(err)
}
pdf.SetXY(50, 50)
pdf.Cell(nil, "你好,世界!")
pdf.SetXY(50, 70)
pdf.Cell(nil, "这是使用go-pdf生成的中文PDF")
pdf.WritePdf("simple.pdf")
}
2. fpdf - 功能丰富
package main
import (
"github.com/jung-kurt/gofpdf"
"fmt"
)
func main() {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddPage()
// 设置字体
pdf.SetFont("Arial", "B", 16)
pdf.Cell(40, 10, "Hello, World!")
// 创建表格
pdf.Ln(12)
pdf.SetFont("Arial", "", 12)
headers := []string{"ID", "Name", "Score"}
data := [][]string{
{"1", "Alice", "95"},
{"2", "Bob", "88"},
{"3", "Charlie", "92"},
}
// 绘制表格
for _, header := range headers {
pdf.CellFormat(40, 7, header, "1", 0, "C", false, 0, "")
}
pdf.Ln(-1)
for _, line := range data {
for _, cell := range line {
pdf.CellFormat(40, 6, cell, "1", 0, "L", false, 0, "")
}
pdf.Ln(-1)
}
// 添加链接
pdf.Ln(10)
pdf.SetFont("", "U", 12)
pdf.SetTextColor(0, 0, 255)
pdf.WriteLinkString(10, 50, "Visit Google", "https://google.com")
err := pdf.OutputFileAndClose("table.pdf")
if err != nil {
fmt.Println("Error:", err)
}
}
3. unidoc - 企业级解决方案
package main
import (
"github.com/unidoc/unipdf/v3/creator"
"github.com/unidoc/unipdf/v3/model"
"log"
)
func main() {
c := creator.New()
// 创建封面
c.CreateFrontPage(func(args creator.FrontpageFunctionArgs) {
p := args.Page
r := args.Rect
// 添加标题
para := c.NewStyledParagraph()
para.SetWidth(r.Width)
para.SetTextAlignment(creator.TextAlignmentCenter)
title := para.Append("公司报告")
title.Style.FontSize = 30
title.Style.Color = creator.ColorRGBFrom8bit(0, 0, 0)
p.Draw(para, creator.DrawRect{
X: r.X,
Y: r.Y + 300,
W: r.Width,
H: 50,
})
})
// 添加内容页
c.NewPage()
p := c.NewParagraph("这是报告正文内容")
p.SetFontSize(12)
p.SetPos(50, 50)
c.Draw(p)
// 添加表格
table := c.NewTable(3)
table.SetColumnWidths(0.2, 0.5, 0.3)
// 表头
headers := []string{"ID", "名称", "价格"}
for _, h := range headers {
cell := table.NewCell()
p := c.NewParagraph(h)
p.SetFontSize(10)
p.SetColor(creator.ColorRGBFrom8bit(255, 255, 255))
cell.SetBackgroundColor(creator.ColorRGBFrom8bit(0, 0, 150))
cell.SetBorder(creator.CellBorderSideAll, creator.CellBorderStyleSingle, 1)
cell.SetContent(p)
}
// 表格数据
data := [][]string{
{"1", "产品A", "$100"},
{"2", "产品B", "$200"},
{"3", "产品C", "$300"},
}
for _, row := range data {
for _, cellData := range row {
cell := table.NewCell()
p := c.NewParagraph(cellData)
p.SetFontSize(10)
cell.SetBorder(creator.CellBorderSideAll, creator.CellBorderStyleSingle, 1)
cell.SetContent(p)
}
}
table.SetPos(50, 100)
c.Draw(table)
err := c.WriteToFile("report.pdf")
if err != nil {
log.Fatal(err)
}
}
4. HTML 转 PDF 方案
wkhtmltopdf 集成
package main
import (
"fmt"
"os/exec"
"strings"
)
func generatePDFWithWKHTML(htmlContent, outputPath string) error {
// 将HTML内容保存为临时文件
tmpfile := "/tmp/template.html"
err := os.WriteFile(tmpfile, []byte(htmlContent), 0644)
if err != nil {
return err
}
// 调用wkhtmltopdf
cmd := exec.Command("wkhtmltopdf",
"--page-size", "A4",
"--orientation", "Portrait",
"--margin-top", "15mm",
"--margin-right", "15mm",
"--margin-bottom", "15mm",
"--margin-left", "15mm",
tmpfile, outputPath)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("wkhtmltopdf error: %s, output: %s", err, string(output))
}
return nil
}
func main() {
html := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; }
h1 { color: #3366cc; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>销售报告</h1>
<table>
<tr><th>产品</th><th>数量</th><th>价格</th></tr>
<tr><td>产品A</td><td>10</td><td>$100</td></tr>
<tr><td>产品B</td><td>5</td><td>$200</td></tr>
</table>
</body>
</html>`
err := generatePDFWithWKHTML(html, "sales_report.pdf")
if err != nil {
fmt.Println("Error:", err)
}
}
chromedp 方案
package main
import (
"context"
"io/ioutil"
"log"
"time"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)
func generatePDFWithChrome(url, outputPath string) error {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var buf []byte
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.WaitReady("body"),
chromedp.ActionFunc(func(ctx context.Context) error {
var err error
buf, err = page.PrintToPDF().
WithPrintBackground(true).
WithPaperWidth(8.27). // A4 width in inches
WithPaperHeight(11.69). // A4 height in inches
WithMarginTop(0.5).
WithMarginBottom(0.5).
WithMarginLeft(0.5).
WithMarginRight(0.5).
Do(ctx)
return err
}),
)
if err != nil {
return err
}
return ioutil.WriteFile(outputPath, buf, 0644)
}
// 从HTML字符串生成PDF
func generatePDFFromHTML(htmlContent, outputPath string) error {
// 创建临时HTML文件
tmpfile := "/tmp/chrome_temp.html"
if err := ioutil.WriteFile(tmpfile, []byte(htmlContent), 0644); err != nil {
return err
}
return generatePDFWithChrome("file://"+tmpfile, outputPath)
}
func main() {
html := `<!DOCTYPE html><html><body><h1>Hello ChromeDP!</h1></body></html>`
err := generatePDFFromHTML(html, "chrome_output.pdf")
if err != nil {
log.Fatal(err)
}
}
高级应用场景
1. 动态报表生成
package main
import (
"github.com/jung-kurt/gofpdf"
"time"
)
type ReportData struct {
Title string
Date time.Time
Items []ReportItem
Summary string
}
type ReportItem struct {
Name string
Value float64
Change float64
}
func GenerateReport(data ReportData, filename string) error {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddPage()
// 标题
pdf.SetFont("Arial", "B", 16)
pdf.CellFormat(0, 10, data.Title, "", 1, "C", false, 0, "")
pdf.Ln(5)
// 日期
pdf.SetFont("Arial", "", 12)
pdf.CellFormat(0, 10, data.Date.Format("2006年01月02日"), "", 1, "R", false, 0, "")
pdf.Ln(10)
// 表格
pdf.SetFont("Arial", "B", 12)
pdf.CellFormat(70, 10, "项目", "1", 0, "C", false, 0, "")
pdf.CellFormat(40, 10, "数值", "1", 0, "C", false, 0, "")
pdf.CellFormat(40, 10, "变化", "1", 1, "C", false, 0, "")
pdf.SetFont("Arial", "", 12)
for _, item := range data.Items {
pdf.CellFormat(70, 8, item.Name, "1", 0, "L", false, 0, "")
pdf.CellFormat(40, 8, fmt.Sprintf("%.2f", item.Value), "1", 0, "R", false, 0, "")
// 根据变化值设置颜色
if item.Change >= 0 {
pdf.SetTextColor(0, 128, 0) // 绿色
} else {
pdf.SetTextColor(255, 0, 0) // 红色
}
pdf.CellFormat(40, 8, fmt.Sprintf("%.2f%%", item.Change), "1", 1, "R", false, 0, "")
pdf.SetTextColor(0, 0, 0) // 恢复黑色
}
// 总结
pdf.Ln(10)
pdf.SetFont("Arial", "I", 12)
pdf.MultiCell(0, 8, data.Summary, "", "L", false)
return pdf.OutputFileAndClose(filename)
}
2. PDF 合并与处理
package main
import (
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
"log"
)
// MergePDFs 合并多个PDF文件
func MergePDFs(inputPaths []string, outputPath string) error {
return api.MergeCreateFile(inputPaths, outputPath, pdfcpu.NewDefaultConfiguration())
}
// ExtractPages 从PDF中提取指定页面
func ExtractPages(inputPath, outputPath string, pages []string) error {
return api.ExtractPagesFile(inputPath, outputPath, pages, pdfcpu.NewDefaultConfiguration())
}
// AddWatermark 添加水印
func AddWatermark(inputPath, outputPath, watermarkText string) error {
config := pdfcpu.NewDefaultConfiguration()
// 创建水印
wm, err := pdfcpu.ParseTextWatermarkDetails(watermarkText, "scale:0.8, rotation:45, opacity:0.2", pdfcpu.POINTS)
if err != nil {
return err
}
return api.AddWatermarksFile(inputPath, outputPath, nil, wm, config)
}
func main() {
// 合并PDF示例
files := []string{"file1.pdf", "file2.pdf"}
err := MergePDFs(files, "merged.pdf")
if err != nil {
log.Fatal(err)
}
// 添加水印示例
err = AddWatermark("document.pdf", "document_watermarked.pdf", "CONFIDENTIAL")
if err != nil {
log.Fatal(err)
}
}
性能优化建议
- 批量处理:对于大量 PDF 生成任务,使用缓冲和批量处理减少 I/O 操作
- 资源复用:复用字体、模板等资源,避免重复加载
- 并发处理:合理使用 goroutine 并行生成多个 PDF
- 缓存机制:对静态内容生成的 PDF 实施缓存策略
- 选择合适的库:根据需求选择最合适的库,避免功能过剩
// 并发生成PDF示例
func GeneratePDFsConcurrently(templates []TemplateData) {
var wg sync.WaitGroup
sem := make(chan struct{}, 10) // 限制并发数
for i, template := range templates {
wg.Add(1)
sem <- struct{}{}
go func(index int, data TemplateData) {
defer wg.Done()
defer func() { <-sem }()
filename := fmt.Sprintf("output_%d.pdf", index)
err := GeneratePDF(data, filename)
if err != nil {
log.Printf("Error generating %s: %v", filename, err)
}
}(i, template)
}
wg.Wait()
}
结论与推荐
根据不同的应用场景,推荐以下选择:
- 简单文本 PDF:使用
go-pdf
,轻量且简单 - 表格和复杂布局:选择
fpdf
,功能丰富且稳定 - 企业级应用:考虑
unidoc
(注意许可证)或pdfcpu
- HTML 转 PDF:
- 简单场景:
wkhtmltopdf
- 需要 JavaScript 渲染:
chromedp
- 简单场景:
- PDF 处理操作:使用
pdfcpu
进行合并、拆分、水印等操作
在实际项目中,可以考虑组合使用多个库,例如使用fpdf
生成内容,使用pdfcpu
进行后期处理,以达到最佳的效果和性能平衡。