How to generate PDF in Go

How to generate PDF in Go

Crafting a web application in Golang

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:

  1. creates a temporary file and creates a temporary file with our html contents

  2. creates another temporary file (just to get the name) and removes it

  3. invokes the weasyprint command

  4. 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.

❤️ Follow me on X or LinkedIn

💡
Do you know a better way to generate PDF in Go?