-
Notifications
You must be signed in to change notification settings - Fork 1
/
pdf.go
200 lines (188 loc) · 5.23 KB
/
pdf.go
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// lazypress converts HTML pages to PDF looking just like they would render in the browser.
// Under-the-hood, it is using Google Chrome (via [github.com/chromedp/chromedp]) to load the HTML so the PDF looks just like if you would print it from the browser.
// It also comes with a built-in HTML sanitizer (uses [github.com/microcosm-cc/bluemonday]).
// You can use this code as a library, or you can run it as a server which will spit out PDFs when you send HTML to it.
package lazypress
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"reflect"
"strconv"
"strings"
"github.com/chromedp/cdproto/page"
"github.com/microcosm-cc/bluemonday"
)
// PDF represents a PDF document as it is used by lazypress.
type PDF struct {
Content []byte
Settings page.PrintToPDFParams
Exporter io.Writer
Closer io.Closer
filePath string
Sanitize bool
}
// Export outputs the generated PDF to the configured output.
// See LoadSettings to configure the output type.
func (p PDF) Export() error {
if p.Exporter == nil {
return fmt.Errorf("no exporter set")
}
_, err := p.Exporter.Write(p.Content)
if err != nil {
return fmt.Errorf("could not export PDF: %v", err)
}
log.Println("PDF exported")
if p.filePath != "" {
log.Println("PDF saved to", p.filePath)
}
if p.Closer != nil {
p.Closer.Close()
}
return nil
}
func (p *PDF) createFile(filename string) (io.WriteCloser, error) {
dir, err := os.UserHomeDir()
if err != nil {
log.Println(err)
// falback to tmp dir
dir = os.TempDir()
}
if filename == "" {
filename = "lazypress"
}
if !strings.HasSuffix(filename, "*.pdf") {
filename = filename + "*.pdf"
}
file, err := ioutil.TempFile(dir, filename)
if err != nil {
log.Fatal(err)
}
p.filePath = file.Name()
return file, nil
}
// LoadSettings loads a map[string]string of settings to configure the PDF.
// The map can contain the following keys:
// - output: the output type. Can be "file", "download".
// - filename: the filename to use when outputting to a file.
// - sanitize: whether to sanitize the HTML.
// Since we are also using the same settings as the [github.com/chromedp/cdproto/page], you can also use the same keys.
// See https://pkg.go.dev/github.com/chromedp/cdproto/page#PrintToPDFParams for more information.
func (p *PDF) LoadSettings(params map[string]string, w io.Writer, c io.Closer) error {
if err := queryParamsToStruct(params, &p.Settings, "json"); err != nil {
return err
}
if strings.ToLower(params["sanitize"]) == "true" {
p.Sanitize = true
if p.Settings.HeaderTemplate != "" {
p.Settings.HeaderTemplate = string(SanitizeHTML([]byte(p.Settings.HeaderTemplate)))
}
if p.Settings.FooterTemplate != "" {
p.Settings.FooterTemplate = string(SanitizeHTML([]byte(p.Settings.FooterTemplate)))
}
}
outputType := strings.ToLower(params["output"])
switch outputType {
case "file":
file, err := p.createFile(params["filename"])
if err != nil {
p.Exporter = w
p.Closer = c
return nil
}
p.Exporter = file
p.Closer = file
case "download":
p.Exporter = w
p.Closer = c
case "s3":
// TODO: implement
p.Exporter = w
p.Closer = c
case "email":
// TODO: implement
p.Exporter = w
p.Closer = c
default:
if w != nil {
p.Exporter = w
p.Closer = c
} else {
p.Exporter = os.Stdout
p.Closer = os.Stdout
}
}
return nil
}
// SanitizeHTML sanitizes the HTML using [github.com/microcosm-cc/bluemonday].
func SanitizeHTML(c []byte) []byte {
policy := bluemonday.UGCPolicy()
policy.AllowElements("html", "head", "title", "body", "style")
policy.AllowAttrs("style").OnElements("body", "table", "tr", "td", "p", "a", "font", "image")
policy.AllowAttrs("name").OnElements("meta")
policy.AllowAttrs("content").OnElements("meta")
return policy.SanitizeBytes(c)
}
func queryParamsToStruct(params map[string]string, structToUse any, tagStr string) error {
// From https://medium.com/wesionary-team/reflections-tutorial-query-string-to-struct-parser-in-go-b2f858f99ea1
var err error
dType := reflect.TypeOf(structToUse)
if dType.Elem().Kind() != reflect.Struct {
return errors.New("input must be a struct")
}
dValue := reflect.ValueOf(structToUse)
for i := 0; i < dType.Elem().NumField(); i++ {
field := dType.Elem().Field(i)
key := strings.Replace(field.Tag.Get(tagStr), ",omitempty", "", -1)
kind := field.Type.Kind()
settingVal, ok := params[key]
if !ok {
continue
}
fieldVal := dValue.Elem().Field(i)
switch kind {
case reflect.String:
if fieldVal.CanSet() {
fieldVal.SetString(settingVal)
}
case reflect.Int:
intVal, err := strconv.ParseInt(settingVal, 10, 64)
if err != nil {
return err
}
if fieldVal.CanSet() {
fieldVal.SetInt(intVal)
}
case reflect.Bool:
boolVal, err := strconv.ParseBool(settingVal)
if err != nil {
return err
}
if fieldVal.CanSet() {
fieldVal.SetBool(boolVal)
}
case reflect.Float64:
floatVal, err := strconv.ParseFloat(settingVal, 64)
if err != nil {
return err
}
if fieldVal.CanSet() {
fieldVal.SetFloat(floatVal)
}
case reflect.Struct:
if fieldVal.CanSet() {
val := reflect.New(field.Type)
err := json.Unmarshal([]byte(settingVal), val.Interface())
if err != nil {
return err
}
fieldVal.Set(val.Elem())
}
}
}
return err
}