Go 利用 chromedp 生成 pdf

Package chromedp is a faster, simpler way to drive browsers supporting the Chrome DevTools Protocol in Go without external dependencies.

可以查看官方的示例

以下示例用的版本为 github.com/chromedp/chromedp v0.8.4

👇 有时需要打印一个在线页面成 pdf,比如把https://www.baidu.com/这个页面打印成 pdf,如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"net/http"

	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/chromedp"
	"github.com/gin-gonic/gin"
	"github.com/pkg/errors"
)

func dp(c *gin.Context) {
	var (
		err error
		buf = make([]byte, 0)
	)

	ctx, cancel := chromedp.NewContext(context.Background())
	defer cancel()
	err = chromedp.Run(ctx, chromedp.Tasks{
		chromedp.Navigate("https://www.baidu.com/"),
		chromedp.ActionFunc(func(ctx context.Context) error {
			buf, _, err = page.PrintToPDF().
				WithPrintBackground(true).
				Do(ctx)
			return err
		}),
	})

	if err != nil {
		err = errors.Wrapf(err, "chromedp Run failed")
		c.JSON(http.StatusInternalServerError, gin.H{
			"msg": err.Error(),
		})
		return
	}

	buffer := &bytes.Buffer{}
	buffer.WriteString("\xEF\xBB\xBF") // 防止中文乱码
	writer := bufio.NewWriter(buffer)

	_, err = writer.Write(buf)
	if err != nil {
		err = errors.Wrapf(err, "bufio Write err")
		c.JSON(http.StatusInternalServerError, gin.H{
			"msg": err.Error(),
		})
		return
	}

	_ = writer.Flush()
	fileName := fmt.Sprintf("1111.pdf")

	c.Header("Content-Type", "text/pdf")
	c.Header("Content-Disposition", "attachment;filename="+fileName)

	_, _ = c.Writer.Write(buffer.Bytes())
	return
}

func main() {
	route := gin.New()
	route.GET("/dp", dp)

	route.Run(":8080")
}

有时需要将一个本地的 html 文件渲染后,提供下载链接,下载成一个 pdf 格式的文件。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package main

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"html/template"
	"net/http"
	"net/http/httptest"
	"os"

	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/chromedp"
	"github.com/gin-gonic/gin"
	"github.com/pkg/errors"
)

type Content struct {
	ChannelLogo     template.URL
	ProductLogo     template.URL
	Title           string
	Content         string
	UserName        string
	OrgName         string
	ChannelName     string
	Number          string
	Date            string
	BackgroundImage template.URL
}

func writeHTML(content Content) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")

		var (
			wd   string
			err  error
			tmpl *template.Template
		)

		wd, _ = os.Getwd()

		tmpl, err = template.ParseFiles(wd + "/tmp.html")
		if err != nil {
			err = errors.Wrapf(err, "template.ParseFiles err")
			_, _ = w.Write([]byte(err.Error()))
			return
		}

		buffer := &bytes.Buffer{}

		err = tmpl.Execute(buffer, content)
		if err != nil {
			err = errors.Wrapf(err, "tmpl.Execute err")
			_, _ = w.Write([]byte(err.Error()))
			return
		}

		_, _ = w.Write(buffer.Bytes())
	})
}

func dp(c *gin.Context) {
	var (
		err error
		buf = make([]byte, 0)
	)

	ctx, cancel := chromedp.NewContext(context.Background())
	defer cancel()

	mux := http.NewServeMux()
	mux.Handle("/pre", writeHTML(Content{
		ChannelLogo:     "",
		ProductLogo:     "",
		Title:           "我是title",
		Content:         "我是内容",
		UserName:        "1111",
		OrgName:         "2222",
		ChannelName:     "3333",
		Number:          "4444",
		Date:            "2006-01-02 15:04:05",
		BackgroundImage: "",
	}))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	url := fmt.Sprintf("%s/pre", ts.URL)

	err = chromedp.Run(ctx, chromedp.Tasks{
		chromedp.Navigate(url),
		chromedp.WaitReady("body"),
		chromedp.ActionFunc(func(ctx context.Context) error {
			var err error
			buf, _, err = page.PrintToPDF().
				WithPrintBackground(true).
				WithPageRanges("1").
				Do(ctx)
			return err
		}),
	})

	if err != nil {
		err = errors.Wrapf(err, "chromedp Run failed")
		c.JSON(http.StatusInternalServerError, gin.H{
			"msg": err.Error(),
		})
		return
	}

	buffer := &bytes.Buffer{}
	buffer.WriteString("\xEF\xBB\xBF") // 防止中文乱码
	writer := bufio.NewWriter(buffer)

	_, err = writer.Write(buf)
	if err != nil {
		err = errors.Wrapf(err, "bufio Write err")
		c.JSON(http.StatusInternalServerError, gin.H{
			"msg": err.Error(),
		})
		return
	}

	_ = writer.Flush()
	fileName := fmt.Sprintf("1111.pdf")

	c.Header("Content-Type", "text/pdf")
	c.Header("Content-Disposition", "attachment;filename="+fileName)

	_, _ = c.Writer.Write(buffer.Bytes())
	return
}

func main() {
	route := gin.New()
	route.GET("/dp", dp)

	route.Run(":8080")
}

本地文件 tmp.html:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .root {
            width: 210mm;
            height: 276mm;
            padding: 0;
            margin: 0 auto;
            background-color: white;
            /*background-image: url();*/
            /* background-size: 210mm 276mm; */
            position: relative;
            overflow: hidden;
        }

        .background-box {
            position: absolute;
            top: 0;
            left: 0;
            bottom: 0;
            right: 0;
            z-index: 0;
        }

        .background-box img {
            width: 210mm;
        }

        .container {
            position: absolute;
            top: 0;
            left: 0;
            bottom: 0;
            right: 0;
            z-index: 1;
            /*border: 1px solid #000;*/
        }

        .title {
            font-size: 38px;
            text-align: center;
            margin-top: 100px;
        }

        .content {
            margin-top: 50px;
            font-size: 22px;
            line-height: 2em;
            padding: 0 108px;
        }

        .content p {
            text-indent: 2em;
            white-space: normal;
            word-break: break-all;
        }

        p strong {
            font-weight: normal;
            text-indent: 0;
            padding: 2px 1em;
            border-bottom: 1px solid #000;
        }

        .logo-box {
            padding: 130px 100px 0 100px;
            display: flex;
            justify-content: space-between;
        }

        .logo-box img {
            max-width: 260px;
        }

        .signature {
            padding: 36px 100px 0 100px;
            font-size: 22px;
            display: flex;
            justify-content: space-between;
            text-align: center;
        }

        .signature p {
            line-height: 2em;
            width: 48%;
        }

        .footer {
            font-size: 14px;
            position: absolute;
            bottom: 36px;
            left: 100px;
            right: 100px;
            top: auto;
            line-height: 1.8em;
        }

        @media screen {
            .root {
                width: 210mm;
                height: 276mm;
                display: flex;
                flex-direction: column;
            }
        }

        @media print {
            .root {
                width: 210mm;
                height: 276mm;
                display: flex;
                flex-direction: column;
            }
        }
    </style>
</head>
<body>
<div class="root">
    <div class="background-box">
        <img src="{{.BackgroundImage}}"
             alt="">
    </div>
    <div class="container">
        <div class="logo-box">
            <div><img
                    src="{{.ChannelLogo}}"
                    alt=""></div>
            <div><img
                    src="{{.ProductLogo}}"
                    alt=""></div>
        </div>
        <div class="title">{{.Title}}</div>
        <div class="content">
            {{.Content}}
        </div>
        <section class="signature">
            <p>{{.ChannelName}}<br/>{{.Date}}</p>
            <p>{{.OrgName}}<br/>{{.Date}}</p>
        </section>
        <section class="footer">
            <p>声明:</p>
            <p style="text-indent: 2em;">
                XXX不对{产品}升级引起的不兼容性负责,牛逼公司不对由XXX产品升级引起的不兼容性负责。此认证书仅适用于牛逼公司现有产品及XXX现有产品(如上所列)。</p>
            <p>证书编号:{{.Number}}</p>
        </section>
    </div>
</div>
</body>
</html>