In this article I am going to show you one way of generating PDF documents using Go. Generating a PDF document is a common need in many applications.
We are building an invoice management application and one of the requirements is the to generate a PDF.
This blog post is part of the Building a Web App with Golang .
From HTML to PDF
In order to create a PDF document we will first create an HTML document and use the weasyprint tool to generate the PDF from the HTML .
There are some golang libraries out there but I found that weasyprint works very nice and haven't run into any issue yet.
Let's first install weasyprint in our machine and see how it works.
Please see the detailed instructions in weasyprint's documentation .
Here is how you can install by utilizing python PIP:
pip install weasyprint
In the documentation there are instructions for many operating systems. Please check there if you run into issues with the above.
Now let's try a demo to verify that it works.
Writing our pdf generator
First let's create a new git branch
git checkout -b invoice-generation
Then create a folder named templates
.
Ins
Create an HTML file named invoice.pdf.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice 1001/24</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
display: flex;
justify-content: center;
}
.container {
width: 80%;
max-width: 800px;
}
.header {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.seller {
text-align: right;
}
.invoice-info {
text-align: right;
margin-bottom: 20px;
}
.buyer {
margin-top: 20px;
}
.line-items {
margin-bottom: 20px;
}
.line-items table {
width: 100%;
border-collapse: collapse;
}
.line-items th, .line-items td {
border: 1px solid #000;
padding: 8px;
text-align: left;
}
.totals {
margin-bottom: 20px;
}
.totals table {
width: 100%;
border-collapse: collapse;
}
.totals td {
padding: 8px;
text-align: right;
}
.totals .total {
font-weight: bold;
}
.footer {
border-top: 1px solid #000;
padding-top: 20px;
text-align: left;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="buyer">
<h3>Bill To:</h3>
<p>Buyer LTD</p>
<p>Buyer Address</p>
<p>VAT: Vat-number</p>
</div>
<div class="seller">
<h2>Seller</h2>
<p>Seller Address</p>
<p>Seller Address2</p>
<p>T.I.C. No: Seller Tax Number</p>
<p>V.A.T. No: Seller Vat Number</p>
</div>
</div>
<div class="invoice-info">
<h2>Tax Invoice No.: 1001/24</h2>
<p>Invoice Date: 02/01/2024</p>
</div>
<div class="line-items">
<table>
<tr>
<th>Description</th>
<th>Amount (€)</th>
</tr>
<tr>
<td>Service Description</td>
<td>1,000.00</td>
</tr>
</table>
</div>
<div class="totals">
<table>
<tr>
<td>Fees</td>
<td>€1,000.00</td>
</tr>
<tr>
<td>VAT</td>
<td>€0</td>
</tr>
<tr class="total">
<td>Total</td>
<td>€1,000.00</td>
</tr>
</table>
</div>
<div class="footer">
<h3>Invoice Payable within 14 days to:</h3>
<p>Bank Name</p>
<p>IBAN: IBAN</p>
<p>BIC: Bic Number</p>
<p>Account Owner Name</p>
</div>
</div>
</body>
</html>
DISCLAIMER: I used AI (ChatGPT) to assist creating the HTML
Let's see how it looks:
The HTML and CSS needs some polishing, but let's continue.
Now we try to create a pdf from the command line to verify that weasyprint works .
weasyprint templates/invoice.pdf.html invoice.pdf
this will create a pdf document. It looks like that:
Creating a Go package that wraps the weasyprint command
Create a folder pkg/pdfgen
mkdir -p pkg/pdfgen
and add a file pdfgen.go
Now in our pdfgen.go:
package pdfgen
import (
"context"
"io"
"os"
"os/exec"
)
func Generate(ctx context.Context, w io.Writer, html []byte) error {
tempHTMLFile, err := os.CreateTemp("", "*.html")
if err != nil {
return err
}
defer os.Remove(tempHTMLFile.Name())
if _, err = tempHTMLFile.Write(html); err != nil {
return err
}
if err = tempHTMLFile.Close(); err != nil {
return err
}
tempPDFFile, err := os.CreateTemp("", "*.pdf")
if err != nil {
return err
}
tempPDFFilePath := tempPDFFile.Name()
defer os.Remove(tempPDFFilePath)
if err = tempPDFFile.Close(); err != nil {
return err
}
cmd := exec.CommandContext(ctx, "weasyprint", tempHTMLFile.Name(), tempPDFFilePath)
err = cmd.Run()
if err != nil {
return err
}
pdfFile, err := os.Open(tempPDFFilePath)
if err != nil {
return err
}
defer pdfFile.Close()
if _, err = io.Copy(w, pdfFile); err != nil {
return err
}
return nil
}
Let's explain what we did:
the function Generate
accepts as the context and
and io.Writer and a slice of bytes.
- The slice of bytes contains the HTML we want to convert to PDF
The io.Writer is the "place" that we are going to write the contents of the PDF
We chose to use an
io.Writer
since this will make our package for versatile. We can pass in a file an http.ResponseWriter or everything that implements that interface
The above function works as follows:
creates a temporary file and creates a temporary file with our html contents
creates another temporary file (just to get the name) and removes it
invokes the weasyprint command
reads the file that weasyprint created (the PDF) and copies the contents to the io.Writer we passed as argument
Step 2 might be replaced by creating a random string. But then we have to compute the temporary directory which is not the same for all systems or configurations. Since Go handles these for us we just (ab)use that .
Now let's write a unit test.
let's run our tests:
go test -v ./...
Let's commit our work so far
git add pkg/ templates/
git commit -m "library that generates pdf from html"
We haven't used our new package in our freelance invoice management project yet. We only wrote a library and we will utilize it in the next article
Find the code in the article's branch in github
Conclusion
In this article we created a small go package that will help to generate PDFs in Golang. Basically, we just wrote a wrapper over a command line tool that is available in most platforms.
I found that creating PDF this way works very well in Go. There are some Go libraries like pdfcpu but for the purpose of creating a PDF out of an HTML I found that weasyprint works very well.
In the next article we will integrate our newly created pdfgen package in our application in order to generate PDFs from our invoices.