Go 语言 PDF 生成库综合比较与实践指南
概述
在 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 mainimport ("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 mainimport ("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 mainimport ("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.Pager := args.Rect// 添加标题para := c.NewStyledParagraph()para.SetWidth(r.Width)para.SetTextAlignment(creator.TextAlignmentCenter)title := para.Append("公司报告")title.Style.FontSize = 30title.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 mainimport ("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}// 调用wkhtmltopdfcmd := 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 mainimport ("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 []byteerr := chromedp.Run(ctx,chromedp.Navigate(url),chromedp.WaitReady("body"),chromedp.ActionFunc(func(ctx context.Context) error {var err errorbuf, err = page.PrintToPDF().WithPrintBackground(true).WithPaperWidth(8.27). // A4 width in inchesWithPaperHeight(11.69). // A4 height in inchesWithMarginTop(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 mainimport ("github.com/jung-kurt/gofpdf""time"
)type ReportData struct {Title stringDate time.TimeItems []ReportItemSummary string
}type ReportItem struct {Name stringValue float64Change 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 mainimport ("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.WaitGroupsem := 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
进行后期处理,以达到最佳的效果和性能平衡。