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
![software development workshops](https://cyrexenterprise.com/wp-content/uploads/2024/05/magicmedia_Maintain_the_core_concept_of_a_bustling_workshop_wit_71e14bd1-d3bb-4e87-8c28-d35c0cc4074c.png)
Cyrex Enterprise Workshops: Turning Dreams into Achievable Action
We often find ourselves talking to clients with a fantastic business idea but feel overwhe...
Read more![Secure web applications](https://cyrexenterprise.com/wp-content/uploads/2024/04/Blog-art.jpg)
Building Secure Web Applications in the Age of Cyber Threats: Essential Strategies
Build secure web applications that withstand evolving threats. Learn secure coding practic...
Read more![](https://cyrexenterprise.com/wp-content/uploads/2024/03/Erick-Rettozi.jpg)
Infrastructure, Blockchain, and Scalability with Erick Rettozi
In this article, Cyrex' Lead Backend Engineer Erick Retozzi offers expert advice on buildi...
Read more![](https://cyrexenterprise.com/wp-content/uploads/2024/03/magicmedia_depict_multiple_devices_and_application_using_APIs_t_d817b6bf-bfc3-411e-91ba-e674bca10758.png)
The Hidden Costs of DIY API Integration: When to Consider Professional Help
Avoid costly mistakes with professional API integration. Learn the hidden costs of DIY API...
Read more