/
report.go
284 lines (260 loc) · 7.87 KB
/
report.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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package tps
import (
"encoding/base64"
"fmt"
"io"
"math"
"os"
"path"
"strings"
"github.com/jung-kurt/gofpdf"
)
// Report is the main struct type that holds all information to generate a PDF.
type Report struct {
Grid Grid
Pdf *gofpdf.Fpdf
Styles map[string]Style
Blocks map[string]Block
FontSourcePath string
FontCompiledPath string
}
func NewReport() *Report {
report := new(Report)
report.Styles = make(map[string]Style)
report.Blocks = make(map[string]Block)
return report
}
// Place a string based on the x, y coordinates on the grid, using the named
// block and style specifications. Returns the # of lines (different from
// Block.Height) taken up by this call to help dynamically place following
// content.
func (r *Report) Content(
x int,
y int,
blockName string,
styleName string,
content string,
) (lineCount int, err error) {
var block Block
var style Style
var ok bool
lineCount = 0
if block, ok = r.Blocks[blockName]; ok == false {
err = fmt.Errorf("Could not find block name in Report: %s", blockName)
return lineCount, err
}
if style, ok = r.Styles[styleName]; ok == false {
err = fmt.Errorf("Could not find style name in Report: %s", styleName)
return lineCount, err
}
point := r.Grid.GetPoint(x, y)
cell := r.Grid.GetCell(block)
r.Pdf.SetFont(style.FontFamily, style.FontStyle, style.FontSize)
r.Pdf.SetXY(point.X, point.Y)
r.Pdf.MultiCell(cell.Width, cell.Height, content, "", style.convertAlignment(), false)
contentLines := strings.Split(content, "\n")
for _, line := range contentLines {
stringWidth := r.Pdf.GetStringWidth(line)
lineCount += int(math.Ceil(stringWidth / cell.Width))
}
lineCount *= block.Height
return lineCount, nil
}
// AddPage creates new page in the report. The previous page is now set if it
// exists, and all placement will take place in this new page.
func (r *Report) AddPage() {
r.Pdf.AddPage()
}
// AddStyle adds a new style to use when placing content in this report.
//
// All specs are set. So even small differences will require different styles.
// An example set of styles can look like the following:
//
// r.AddStyle("header", "OpenSans", "", 24, AlignmentCenter | AlignmentTop)
// r.AddStyle("subheader", "OpenSans", "", 18, AlignmentLeft | AlignmentTop)
func (r *Report) AddStyle(
name string,
fontFamily string,
fontStyle string,
fontSize float64,
alignment int,
) {
r.Styles[name] = Style{
FontFamily: fontFamily,
FontStyle: fontStyle,
FontSize: fontSize,
Alignment: alignment,
}
}
// AddBlock adds a new block specification to use when placing content in this
// report. The width and height are the number of columns and lines of the block
// respectively. The height is the number of lineHeight per line that the
// content placement takes up.
func (r *Report) AddBlock(name string, width, height int) {
r.Blocks[name] = Block{
Width: width,
Height: height,
}
}
// AddFont takes a font filename and compiles it into Report.FontCompiledPath
// with the encoding specified. It strips the filename extension and replaces
// it with .json automatically. The extension-less string becomes the name of
// the font family to use with Report.AddStyle(). For example:
//
// r.AddFont("OpenSans-Bold.ttf", "cp1252")
// r.AddStyle("header", "OpenSans-Bold", "", 64, AlignmentTop | AlignmentLeft)
//
// The following encodings are supported:
//
// cp1250
// cp1251
// cp1252
// cp1253
// cp1254
// cp1255
// cp1257
// cp1258
// cp874
// iso-8859-1
// iso-8859-11
// iso-8859-15
// iso-8859-16
// iso-8859-2
// iso-8859-4
// iso-8859-5
// iso-8859-7
// iso-8859-9
// koi8-r
// koi8-u
func (r *Report) AddFont(filename, encoding string) error {
var err error
err = r.PrepareFontCompiledPath()
if err != nil {
return err
}
ext := path.Ext(filename)
familyName := filename[:len(filename)-len(ext)]
// auto compiles
if path.Ext(filename) == ".json" {
if r.IsCompiledFile(filename) {
r.Pdf.AddFont(familyName, "", filename)
} else {
return fmt.Errorf("Cache font file not found: %s", filename)
}
} else {
if r.IsSourcedFont(filename) {
compiledFilename, err := r.CompileFont(filename, encoding)
if err != nil {
return fmt.Errorf("Could not compile font: %v", err)
}
r.Pdf.AddFont(familyName, "", compiledFilename)
} else {
return fmt.Errorf("Source font file not found: %s", filename)
}
}
return nil
}
// PrepareFontCompiledPath creates the "_compiled" subdirectory.
func (r *Report) PrepareFontCompiledPath() error {
if _, err := os.Stat(path.Join(r.FontCompiledPath)); os.IsNotExist(err) {
err = os.MkdirAll(r.FontCompiledPath, os.ModeDir)
if err != nil {
return err
}
return os.Chmod(r.FontCompiledPath, 0775)
}
return nil
}
// CompileEncoding creates the encoding map file in Report.FontCompiledPath so
// the underlying Fpdf object can correctly use it to compile fonts.
func (r *Report) CompileEncoding(encoding string) (filename string, err error) {
filename = path.Join(r.FontCompiledPath, encoding+".map")
if r.IsCompiledFile(filename) {
return
}
if data, ok := encodings[encoding]; ok {
file, err := os.Create(filename)
if err != nil {
err = fmt.Errorf("Could not open file to compile encoding file: %v", err)
return filename, err
}
defer file.Close()
reader := strings.NewReader(data)
decoder := base64.NewDecoder(base64.StdEncoding, reader)
_, err = io.Copy(file, decoder)
if err != nil {
err = fmt.Errorf("Encoding failed to copy to file: %v", err)
return filename, err
}
} else {
err = fmt.Errorf("Encoding not supported: %s", encoding)
}
return
}
// CompileFont takes a font file in Report.FontSourcePath and converts it into
// .json format if it doesn't exist in Report.FontCompiledPath.
func (r *Report) CompileFont(filename, encoding string) (string, error) {
fontFilename := path.Join(r.FontSourcePath, filename)
// replacing ext with json
extLen := len(path.Ext(filename))
compiledFilename := filename[:len(filename)-extLen] + ".json"
encodingFilename, err := r.CompileEncoding(encoding)
if err != nil {
return compiledFilename, err
}
err = gofpdf.MakeFont(fontFilename, encodingFilename, r.FontCompiledPath, nil, true)
return compiledFilename, err
}
// IsCompiledFile checks if the filename exists in Report.FontCompiledPath. This
// has the suffix "File" instead of "Font" like Report.IsSourcedFont() because
// this method might be used to check encoding map files as well.
func (r *Report) IsCompiledFile(filename string) bool {
_, err := os.Stat(path.Join(r.FontCompiledPath, filename))
return !os.IsNotExist(err)
}
// IsSourcedFont checks if the font filename exists in Report.FontSourcePath.
func (r *Report) IsSourcedFont(filename string) bool {
_, err := os.Stat(path.Join(r.FontSourcePath, filename))
return !os.IsNotExist(err)
}
// SetGrid sets all page and grid related specifications required to place
// content. This must be set before any Content() calls are made.
func (r *Report) SetGrid(
orientation int,
pageSize int,
unit int,
margin float64,
columnCount int,
gutterWidth float64,
lineHeight float64,
) {
fontPath := r.FontSourcePath
r.Grid = Grid{
Orientation: orientation,
PageSize: pageSize,
Unit: unit,
ColumnCount: columnCount,
GutterWidth: gutterWidth,
Margin: margin,
LineHeight: lineHeight,
}
pdf := gofpdf.New(
r.Grid.convertOrientation(),
r.Grid.convertUnit(),
r.Grid.convertPageSize(),
fontPath,
)
r.Pdf = pdf
r.Pdf.SetMargins(margin, margin, margin)
pageWidth, pageHeight := r.Pdf.GetPageSize()
r.Grid.PageWidth = pageWidth
r.Grid.PageHeight = pageHeight
r.Grid.CalculateColumns()
}
// SetFontPath tells the Report where to find fonts specified with AddFont().
func (r *Report) SetFontPath(fontSourcePath string) {
r.FontSourcePath = fontSourcePath
r.FontCompiledPath = path.Join(fontSourcePath, "_compiled")
r.PrepareFontCompiledPath()
r.Pdf.SetFontLocation(r.FontCompiledPath)
}