I recently had to work on a Go project where PDFs had to be generated, with custom data, based on a template.

After considering a few options, I decided to go with wkhtmltopdf’s wrapper github.com/SebastiaanKlippert/go-wkhtmltopdf, since this a solid and widely used open source library and I would only be using a Go layer on top of it.

Also, this would allow the template to be maintained in HTML, which would hopefully end up in less headaches than creating PDF’s with a lower level generator kind of thing.

Cut to the chase

Not directly related… But I defined a interface for this service so it could be mocked, and so on.

package domain

// PDFService represents the interface of a pdf generation service
type PDFService interface {
	GeneratePDF(data *SomeModel) ([]byte, error)
}

Here’s the service itself. It quite self explanatory but still, I’ve added comments on meaningful steps.

It essentially consists of first generating an HTML file based on a template and then pass that HTML to wkhtmltopdf.

package pdf

import (
	"bytes"
	"github.com/SebastiaanKlippert/go-wkhtmltopdf"
	"html/template"
	domain "the-project"
)

type PDFService struct {}

func NewPDFService() *PDFService {
	return &PDFService{}
}

func (p PDFService) GeneratePDF(data *domain.SomeModel) ([]byte, error) {
	var templ *template.Template
	var err error

	// use Go's default HTML template generation tools to generate your HTML
	if templ, err = template.ParseFiles("pdf-template.html"); err != nil {
		return nil, err
	}

	// apply the parsed HTML template data and keep the result in a Buffer
	var body bytes.Buffer
	if err = templ.Execute(&body, data); err != nil {
		return nil, err
	}

	// initalize a wkhtmltopdf generator
	pdfg, err := wkhtmltopdf.NewPDFGenerator()
	if err != nil {
		return nil, err
	}

	// read the HTML page as a PDF page
	page := wkhtmltopdf.NewPageReader(bytes.NewReader(body.Bytes()))

	// enable this if the HTML file contains local references such as images, CSS, etc.
	page.EnableLocalFileAccess.Set(true)

	// add the page to your generator
	pdfg.AddPage(page)

	// manipulate page attributes as needed
	pdfg.MarginLeft.Set(0)
	pdfg.MarginRight.Set(0)
	pdfg.Dpi.Set(300)
	pdfg.PageSize.Set(wkhtmltopdf.PageSizeA4)
	pdfg.Orientation.Set(wkhtmltopdf.OrientationLandscape)

	// magic
	err = pdfg.Create()
	if err != nil {
		return nil, err
	}

	return pdfg.Bytes(), nil
}

Extra

Say you want to serve the PDF in an API response:

func (h *YourHandler) GetPDF(w http.ResponseWriter, r *http.Request) {
	// .....

	pdfBytes, err := h.pdfService.GeneratePDF(&data)
	if err != nil {
		httputil.RespondInternalError(w, r, err)
		return
	}

	w.Header().Set("Content-Disposition", "attachment; filename=kittens.pdf")
	w.Header().Set("Content-Type", "application/pdf")
	w.WriteHeader(http.StatusOK)
	w.Write(pdfBytes)
}