Go 语言 PDF 生成库综合比较与实践指南

发布于:2025-09-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

概述

在 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)
    }
}

性能优化建议

  1. 批量处理:对于大量 PDF 生成任务,使用缓冲和批量处理减少 I/O 操作
  2. 资源复用:复用字体、模板等资源,避免重复加载
  3. 并发处理:合理使用 goroutine 并行生成多个 PDF
  4. 缓存机制:对静态内容生成的 PDF 实施缓存策略
  5. 选择合适的库:根据需求选择最合适的库,避免功能过剩
// 并发生成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()
}

结论与推荐

根据不同的应用场景,推荐以下选择:

  1. 简单文本 PDF:使用go-pdf,轻量且简单
  2. 表格和复杂布局:选择fpdf,功能丰富且稳定
  3. 企业级应用:考虑unidoc(注意许可证)或pdfcpu
  4. HTML 转 PDF
    • 简单场景:wkhtmltopdf
    • 需要 JavaScript 渲染:chromedp
  5. PDF 处理操作:使用pdfcpu进行合并、拆分、水印等操作

在实际项目中,可以考虑组合使用多个库,例如使用fpdf生成内容,使用pdfcpu进行后期处理,以达到最佳的效果和性能平衡。


网站公告

今日签到

点亮在社区的每一天
去签到