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)
}
Author: Diogo Simões, Tech Lead at Cloudoki.
Related Articles
Deploying NestJS Microservices to AWS ECS with Pulumi IaC
Let’s see how we can deploy NestJS microservices with multiple environments to ECS using...
Read moreWhat is CI/CD? A Guide to Continuous Integration & Continuous Delivery
Learn how CI/CD can improve code quality, enhance collaboration, and accelerate time-to-ma...
Read moreBuild a Powerful Q&A Bot with LLama3, LangChain & Supabase (Local Setup)
Harness the power of LLama3 with LangChain & Supabase to create a smart Q&A bot. This guid...
Read moreDemystifying AI: The Power of Simplification
Unleash AI's power without the hassle. Learn how to simplify complex AI tasks through easy...
Read more