...

Source file src/gonum.org/v1/plot/vg/vgpdf/vgpdf.go

Documentation: gonum.org/v1/plot/vg/vgpdf

     1  // Copyright ©2015 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package vgpdf implements the vg.Canvas interface
     6  // using gofpdf (github.com/phpdave11/gofpdf).
     7  package vgpdf // import "gonum.org/v1/plot/vg/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  // codePageEncoding holds informations about the characters encoding of TrueType
    33  // font files, needed by gofpdf to embed fonts in a PDF document.
    34  // We use cp1252 (code page 1252, Windows Western) to encode characters.
    35  // See:
    36  //   - https://en.wikipedia.org/wiki/Windows-1252
    37  //
    38  // TODO: provide a Canvas-level func option to embed fonts with a user provided
    39  // code page schema?
    40  //
    41  //go:embed cp1252.map
    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  // DPI is the nominal resolution of drawing in PDF.
    51  const DPI = 72
    52  
    53  // Canvas implements the vg.Canvas interface,
    54  // drawing to a PDF.
    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  	// Switch to embed fonts in PDF file.
    65  	// The default is to embed fonts.
    66  	// This makes the PDF file more portable but also larger.
    67  	embed bool
    68  }
    69  
    70  type context struct {
    71  	fill  color.Color
    72  	line  color.Color
    73  	width vg.Length
    74  }
    75  
    76  // New creates a new PDF Canvas.
    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  // EmbedFonts specifies whether the resulting PDF canvas should
    97  // embed the fonts or not.
    98  // EmbedFonts returns the previous value before modification.
    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  	// go-fpdf uses the top left corner as origin.
   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  		// not defined (standard font?), use average of 81%
   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  // DrawImage implements the vg.Canvas.DrawImage method.
   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  // font registers a font and a size with the PDF canvas.
   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  // pdfPath processes a vg.Path and applies it to the canvas.
   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  // unit returns a fpdf.Unit, converted from a vg.Length.
   329  func (c *Canvas) unit(l vg.Length) float64 {
   330  	return l.Dots(c.DPI())
   331  }
   332  
   333  // imageName generates a unique image name for this PDF canvas
   334  func (c *Canvas) imageName() string {
   335  	c.numImages++
   336  	return fmt.Sprintf("image_%03d.png", c.numImages)
   337  }
   338  
   339  // WriterCounter implements the io.Writer interface, and counts
   340  // the total number of bytes written.
   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  // WriteTo writes the Canvas to an io.Writer.
   353  // After calling Write, the canvas is closed
   354  // and may no longer be used for drawing.
   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  // rgba converts a Go color into a gofpdf 3-tuple int + 1 float64
   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  // fontKey represents a PDF font request.
   382  // fontKey needs to know whether the font will be embedded or not,
   383  // as gofpdf.MakeFont will generate different informations.
   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  // NextPage creates a new page in the final PDF document.
   479  // The new page is the new current page.
   480  // Modifications applied to the canvas will only be applied to that new page.
   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