1
2
3
4
5
6
7 package vgpdf
8
9 import (
10 "bufio"
11 "bytes"
12 _ "embed"
13 "fmt"
14 "image"
15 "image/color"
16 "image/png"
17 "io"
18 "log"
19 "math"
20 "os"
21 "path/filepath"
22 "sync"
23
24 pdf "github.com/go-pdf/fpdf"
25 stdfnt "golang.org/x/image/font"
26
27 "gonum.org/v1/plot/font"
28 "gonum.org/v1/plot/vg"
29 "gonum.org/v1/plot/vg/draw"
30 )
31
32
33
34
35
36
37
38
39
40
41
42 var codePageEncoding []byte
43
44 func init() {
45 draw.RegisterFormat("pdf", func(w, h vg.Length) vg.CanvasWriterTo {
46 return New(w, h)
47 })
48 }
49
50
51 const DPI = 72
52
53
54
55 type Canvas struct {
56 doc *pdf.Fpdf
57 w, h vg.Length
58
59 dpi int
60 numImages int
61 stack []context
62 fonts map[font.Font]struct{}
63
64
65
66
67 embed bool
68 }
69
70 type context struct {
71 fill color.Color
72 line color.Color
73 width vg.Length
74 }
75
76
77 func New(w, h vg.Length) *Canvas {
78 cfg := pdf.InitType{
79 UnitStr: "pt",
80 Size: pdf.SizeType{Wd: w.Points(), Ht: h.Points()},
81 }
82 c := &Canvas{
83 doc: pdf.NewCustom(&cfg),
84 w: w,
85 h: h,
86 dpi: DPI,
87 stack: make([]context, 1),
88 fonts: make(map[font.Font]struct{}),
89 embed: true,
90 }
91 c.NextPage()
92 vg.Initialize(c)
93 return c
94 }
95
96
97
98
99 func (c *Canvas) EmbedFonts(v bool) bool {
100 prev := c.embed
101 c.embed = v
102 return prev
103 }
104
105 func (c *Canvas) DPI() float64 {
106 return float64(c.dpi)
107 }
108
109 func (c *Canvas) context() *context {
110 return &c.stack[len(c.stack)-1]
111 }
112
113 func (c *Canvas) Size() (w, h vg.Length) {
114 return c.w, c.h
115 }
116
117 func (c *Canvas) SetLineWidth(w vg.Length) {
118 c.context().width = w
119 lw := c.unit(w)
120 c.doc.SetLineWidth(lw)
121 }
122
123 func (c *Canvas) SetLineDash(dashes []vg.Length, offs vg.Length) {
124 ds := make([]float64, len(dashes))
125 for i, d := range dashes {
126 ds[i] = c.unit(d)
127 }
128 c.doc.SetDashPattern(ds, c.unit(offs))
129 }
130
131 func (c *Canvas) SetColor(clr color.Color) {
132 if clr == nil {
133 clr = color.Black
134 }
135 c.context().line = clr
136 c.context().fill = clr
137 r, g, b, a := rgba(clr)
138 c.doc.SetFillColor(r, g, b)
139 c.doc.SetDrawColor(r, g, b)
140 c.doc.SetTextColor(r, g, b)
141 c.doc.SetAlpha(a, "Normal")
142 }
143
144 func (c *Canvas) Rotate(r float64) {
145 c.doc.TransformRotate(-r*180/math.Pi, 0, 0)
146 }
147
148 func (c *Canvas) Translate(pt vg.Point) {
149 xp, yp := c.pdfPoint(pt)
150 c.doc.TransformTranslate(xp, yp)
151 }
152
153 func (c *Canvas) Scale(x float64, y float64) {
154 c.doc.TransformScale(x*100, y*100, 0, 0)
155 }
156
157 func (c *Canvas) Push() {
158 c.stack = append(c.stack, *c.context())
159 c.doc.TransformBegin()
160 }
161
162 func (c *Canvas) Pop() {
163 c.doc.TransformEnd()
164 c.stack = c.stack[:len(c.stack)-1]
165 }
166
167 func (c *Canvas) Stroke(p vg.Path) {
168 if c.context().width > 0 {
169 c.pdfPath(p, "D")
170 }
171 }
172
173 func (c *Canvas) Fill(p vg.Path) {
174 c.pdfPath(p, "F")
175 }
176
177 func (c *Canvas) FillString(fnt font.Face, pt vg.Point, str string) {
178 if fnt.Font.Size == 0 {
179 return
180 }
181
182 c.font(fnt, pt)
183 style := ""
184 if fnt.Font.Weight == stdfnt.WeightBold {
185 style += "B"
186 }
187 if fnt.Font.Style == stdfnt.StyleItalic {
188 style += "I"
189 }
190 c.doc.SetFont(fnt.Name(), style, c.unit(fnt.Font.Size))
191
192 c.Push()
193 defer c.Pop()
194 c.Translate(pt)
195
196 c.Scale(1, -1)
197 left, top, right, bottom := c.sbounds(fnt, str)
198 w := right - left
199 h := bottom - top
200 margin := c.doc.GetCellMargin()
201
202 c.doc.MoveTo(-left-margin, top)
203 c.doc.CellFormat(w, h, str, "", 0, "BL", false, 0, "")
204 }
205
206 func (c *Canvas) sbounds(fnt font.Face, txt string) (left, top, right, bottom float64) {
207 _, h := c.doc.GetFontSize()
208 style := ""
209 if fnt.Font.Weight == stdfnt.WeightBold {
210 style += "B"
211 }
212 if fnt.Font.Style == stdfnt.StyleItalic {
213 style += "I"
214 }
215 d := c.doc.GetFontDesc(fnt.Name(), style)
216 if d.Ascent == 0 {
217
218 top = 0.81 * h
219 } else {
220 top = -float64(d.Ascent) * h / float64(d.Ascent-d.Descent)
221 }
222 return 0, top, c.doc.GetStringWidth(txt), top + h
223 }
224
225
226 func (c *Canvas) DrawImage(rect vg.Rectangle, img image.Image) {
227 opts := pdf.ImageOptions{ImageType: "png", ReadDpi: true}
228 name := c.imageName()
229
230 buf := new(bytes.Buffer)
231 err := png.Encode(buf, img)
232 if err != nil {
233 log.Panicf("error encoding image to PNG: %v", err)
234 }
235 c.doc.RegisterImageOptionsReader(name, opts, buf)
236
237 xp, yp := c.pdfPoint(rect.Min)
238 wp, hp := c.pdfPoint(rect.Size())
239
240 c.doc.ImageOptions(name, xp, yp, wp, hp, false, opts, 0, "")
241 }
242
243
244 func (c *Canvas) font(fnt font.Face, pt vg.Point) {
245 if _, ok := c.fonts[fnt.Font]; ok {
246 return
247 }
248 name := fnt.Name()
249 key := fontKey{font: fnt, embed: c.embed}
250 raw := new(bytes.Buffer)
251 _, err := fnt.Face.WriteSourceTo(nil, raw)
252 if err != nil {
253 log.Panicf("vgpdf: could not generate font %q data for PDF: %+v", name, err)
254 }
255
256 zdata, jdata, err := getFont(key, raw.Bytes(), codePageEncoding)
257 if err != nil {
258 log.Panicf("vgpdf: could not generate font data for PDF: %v", err)
259 }
260
261 c.fonts[fnt.Font] = struct{}{}
262 c.doc.AddFontFromBytes(name, "", jdata, zdata)
263 }
264
265
266 func (c *Canvas) pdfPath(path vg.Path, style string) {
267 var (
268 xp float64
269 yp float64
270 )
271 for _, comp := range path {
272 switch comp.Type {
273 case vg.MoveComp:
274 xp, yp = c.pdfPoint(comp.Pos)
275 c.doc.MoveTo(xp, yp)
276 case vg.LineComp:
277 c.doc.LineTo(c.pdfPoint(comp.Pos))
278 case vg.ArcComp:
279 c.arc(comp, style)
280 case vg.CurveComp:
281 px, py := c.pdfPoint(comp.Pos)
282 switch len(comp.Control) {
283 case 1:
284 cx, cy := c.pdfPoint(comp.Control[0])
285 c.doc.CurveTo(cx, cy, px, py)
286 case 2:
287 cx, cy := c.pdfPoint(comp.Control[0])
288 dx, dy := c.pdfPoint(comp.Control[1])
289 c.doc.CurveBezierCubicTo(cx, cy, dx, dy, px, py)
290 default:
291 panic("vgpdf: invalid number of control points")
292 }
293 case vg.CloseComp:
294 c.doc.LineTo(xp, yp)
295 c.doc.ClosePath()
296 default:
297 panic(fmt.Sprintf("Unknown path component type: %d\n", comp.Type))
298 }
299 }
300 c.doc.DrawPath(style)
301 }
302
303 func (c *Canvas) arc(comp vg.PathComp, style string) {
304 x0 := comp.Pos.X + comp.Radius*vg.Length(math.Cos(comp.Start))
305 y0 := comp.Pos.Y + comp.Radius*vg.Length(math.Sin(comp.Start))
306 c.doc.LineTo(c.pdfPointXY(x0, y0))
307 r := c.unit(comp.Radius)
308 const deg = 180 / math.Pi
309 angle := comp.Angle * deg
310 beg := comp.Start * deg
311 end := beg + angle
312 x := c.unit(comp.Pos.X)
313 y := c.unit(comp.Pos.Y)
314 c.doc.Arc(x, y, r, r, angle, beg, end, style)
315 x1 := comp.Pos.X + comp.Radius*vg.Length(math.Cos(comp.Start+comp.Angle))
316 y1 := comp.Pos.Y + comp.Radius*vg.Length(math.Sin(comp.Start+comp.Angle))
317 c.doc.MoveTo(c.pdfPointXY(x1, y1))
318 }
319
320 func (c *Canvas) pdfPointXY(x, y vg.Length) (float64, float64) {
321 return c.unit(x), c.unit(y)
322 }
323
324 func (c *Canvas) pdfPoint(pt vg.Point) (float64, float64) {
325 return c.unit(pt.X), c.unit(pt.Y)
326 }
327
328
329 func (c *Canvas) unit(l vg.Length) float64 {
330 return l.Dots(c.DPI())
331 }
332
333
334 func (c *Canvas) imageName() string {
335 c.numImages++
336 return fmt.Sprintf("image_%03d.png", c.numImages)
337 }
338
339
340
341 type writerCounter struct {
342 io.Writer
343 n int64
344 }
345
346 func (w *writerCounter) Write(p []byte) (int, error) {
347 n, err := w.Writer.Write(p)
348 w.n += int64(n)
349 return n, err
350 }
351
352
353
354
355 func (c *Canvas) WriteTo(w io.Writer) (int64, error) {
356 c.Pop()
357 c.doc.Close()
358 wc := writerCounter{Writer: w}
359 b := bufio.NewWriter(&wc)
360 if err := c.doc.Output(b); err != nil {
361 return wc.n, err
362 }
363 err := b.Flush()
364 return wc.n, err
365 }
366
367
368 func rgba(c color.Color) (int, int, int, float64) {
369 if c == nil {
370 c = color.Black
371 }
372 r, g, b, a := c.RGBA()
373 return int(r >> 8), int(g >> 8), int(b >> 8), float64(a) / math.MaxUint16
374 }
375
376 type fontsCache struct {
377 sync.RWMutex
378 cache map[fontKey]fontVal
379 }
380
381
382
383
384 type fontKey struct {
385 font font.Face
386 embed bool
387 }
388
389 type fontVal struct {
390 z, j []byte
391 }
392
393 func (c *fontsCache) get(key fontKey) (fontVal, bool) {
394 c.RLock()
395 defer c.RUnlock()
396 v, ok := c.cache[key]
397 return v, ok
398 }
399
400 func (c *fontsCache) add(k fontKey, v fontVal) {
401 c.Lock()
402 defer c.Unlock()
403 c.cache[k] = v
404 }
405
406 var pdfFonts = &fontsCache{
407 cache: make(map[fontKey]fontVal),
408 }
409
410 func getFont(key fontKey, font, encoding []byte) (z, j []byte, err error) {
411 if v, ok := pdfFonts.get(key); ok {
412 return v.z, v.j, nil
413 }
414
415 v, err := makeFont(key, font, encoding)
416 if err != nil {
417 return nil, nil, err
418 }
419 return v.z, v.j, nil
420 }
421
422 func makeFont(key fontKey, font, encoding []byte) (val fontVal, err error) {
423 tmpdir, err := os.MkdirTemp("", "gofpdf-makefont-")
424 if err != nil {
425 return val, err
426 }
427 defer os.RemoveAll(tmpdir)
428
429 indir := filepath.Join(tmpdir, "input")
430 err = os.Mkdir(indir, 0755)
431 if err != nil {
432 return val, err
433 }
434
435 outdir := filepath.Join(tmpdir, "output")
436 err = os.Mkdir(outdir, 0755)
437 if err != nil {
438 return val, err
439 }
440
441 fname := filepath.Join(indir, "font.ttf")
442 encname := filepath.Join(indir, "cp1252.map")
443
444 err = os.WriteFile(fname, font, 0644)
445 if err != nil {
446 return val, err
447 }
448
449 err = os.WriteFile(encname, encoding, 0644)
450 if err != nil {
451 return val, err
452 }
453
454 err = pdf.MakeFont(fname, encname, outdir, io.Discard, key.embed)
455 if err != nil {
456 return val, err
457 }
458
459 if key.embed {
460 z, err := os.ReadFile(filepath.Join(outdir, "font.z"))
461 if err != nil {
462 return val, err
463 }
464 val.z = z
465 }
466
467 j, err := os.ReadFile(filepath.Join(outdir, "font.json"))
468 if err != nil {
469 return val, err
470 }
471 val.j = j
472
473 pdfFonts.add(key, val)
474
475 return val, nil
476 }
477
478
479
480
481 func (c *Canvas) NextPage() {
482 if c.doc.PageNo() > 0 {
483 c.Pop()
484 }
485 c.doc.SetMargins(0, 0, 0)
486 c.doc.AddPage()
487 c.Push()
488 c.Translate(vg.Point{X: 0, Y: c.h})
489 c.Scale(1, -1)
490 }
491
View as plain text