DEV Community

loading...
Cover image for How to create PDFs with Go, Maroto & GoFakeIt

How to create PDFs with Go, Maroto & GoFakeIt

Div Rhino
Creating fun and interesting project-based tutorials. Find Div Rhino on YouTube for video tutorials!
・12 min read

Originally posted on divrhino.com

In this tutorial, we will learn how to use the Maroto package to create a PDF. From invoices to certificates, being able to generate a PDF from app data is a pretty handy feature. In this tutorial, we will use the Maroto package to build and save a product list. We will also learn how we can use the GoFakeIt package to generate some random dummy data that we can use in our PDF.

Prerequisites

To follow along, you will need to have Go installed. We are using version 1.16.2 for this tutorial.

The packages

Maroto

The Maroto package is described as being bootstrap-inspired, but you don't need to know Bootstrap to use it. However if you have used Bootstrap before, the approach Maroto takes will feel familiar to you. A document is built using rows, columns and components. The package gives us the ability to include features such as:

  • images
  • signatures
  • Barcodes, and
  • QR Codes

GoFakeIt

Using mock data is a great way to speed up the prototyping process. We will use the GoFakeIt package to create a little dummy data generator to insert into our PDF.

We will see how to install these in the Getting Started section.

Wireframe of what we're building

We will be creating an product list for an imaginary fruit shop called "Div Rhino Fruit". The document will have a header and a table of products.

Mockup of Div Rhino Fruit List PDF

Getting started

Now that we've covered the basic background information, we can start setting up our project.

Inside our Sites directory, we can make a new folder which we will call fruitful-pdf and we will change into it

cd Sites
mkdir fruitful-pdf
cd fruitful-pdf

Enter fullscreen mode Exit fullscreen mode

We will be using go modules to manage our dependencies.

It is a good idea to name your project using the URL where it can be downloaded, so I'm going to use my Github repo URL as the name of my package. Please feel free to substitute the following command with your own Github account or personal website

go mod init github.com/divrhino/fruitful-pdf
Enter fullscreen mode Exit fullscreen mode

After the command runs successfully, you should see a go.mod file in your project directory.

Next we will install the Maroto package. This tutorial will be using version v0.31.0. If you're using an older version of the Maroto package, certain properties such as text colours may not be available.

go get -u github.com/johnfercher/maroto
go get github.com/johnfercher/maroto/internal@v0.31.0
Enter fullscreen mode Exit fullscreen mode

We will also need to install the GoFakeIt package for our mock data generator. We will be using version v6.3.0 for this tutorial. You can install it with the following command:

go get github.com/brianvoe/gofakeit/v6
Enter fullscreen mode Exit fullscreen mode

After installing these packages, you should see a go.sum file has been created in your project folder. This can be thought of as a lock file. It is used to verify that the checksum of dependencies have not changed.

Those are the only third-party packages we need. We are now ready to start creating our PDF structure.

Skeleton PDF

Like with most Go projects, we can go ahead and create a main.go file at the root of our project directory.

touch main.go
Enter fullscreen mode Exit fullscreen mode

Inside our main.go file, let's import all our necessary Maroto sub-packages. Each package provides us with useful functions that will allow us to use things like colours, images, fonts and components.

package main

func main() {}
Enter fullscreen mode Exit fullscreen mode

Now, inside the body of our func main(), we can create a new maroto using the pdf sub-package. The NewMaroto() method creates a new maroto instance and returns a pointer to pdf.Maroto. It also expects two arguments: (i) orientation, and (ii) paper size. We can get these values from the consts sub-package:

package main

import (
    "github.com/johnfercher/maroto/pkg/consts"
    "github.com/johnfercher/maroto/pkg/pdf"
)

func main() {
    m := pdf.NewMaroto(consts.Portrait, consts.A4)
}
Enter fullscreen mode Exit fullscreen mode

We also want to give our PDF document some margins so the content isn't falling off the sides. We can do this by using the SetPageMargins method, which takes 3 values: (i) a left, (ii) a top, and (iii) a right margin value.

package main

import (
    "github.com/johnfercher/maroto/pkg/consts"
    "github.com/johnfercher/maroto/pkg/pdf"
)

func main() {
    m := pdf.NewMaroto(consts.Portrait, consts.A4)
    m.SetPageMargins(20, 10, 20)
}
Enter fullscreen mode Exit fullscreen mode

Our content isn't ready yet, but we can go ahead and save an empty file for now using the OutputFileAndClose() method. Let's tell it that we want to save the output as div_rhino_fruit.pdf, in a folder called pdfs.

package main

import (
    "github.com/johnfercher/maroto/pkg/consts"
    "github.com/johnfercher/maroto/pkg/pdf"
)

func main() {
    m := pdf.NewMaroto(consts.Portrait, consts.A4)
    m.SetPageMargins(20, 10, 20)

    m.OutputFileAndClose("pdfs/div_rhino_fruit.pdf")
}
Enter fullscreen mode Exit fullscreen mode

We'll have to remember to create the pdfs folder too.

mkdir pdfs
Enter fullscreen mode Exit fullscreen mode

The OutputFileAndClose() method returns an error, so let's do some quick error handling before we move on. If, for some reason, we're unable to output a PDF file, the program immediately aborts, because its only purpose is to create this PDF file.

package main

import (
    "fmt"
    "os"

    "github.com/johnfercher/maroto/pkg/consts"
    "github.com/johnfercher/maroto/pkg/pdf"
)

func main() {
    m := pdf.NewMaroto(consts.Portrait, consts.A4)
    m.SetPageMargins(20, 10, 20)

    err := m.OutputFileAndClose("pdfs/div_rhino_fruit.pdf")
    if err != nil {
        fmt.Println("⚠️  Could not save PDF:", err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

And just so we know something is actually happening, let's print out a little message every time we run our code:

package main

import (
    "fmt"
    "os"

    "github.com/johnfercher/maroto/pkg/consts"
    "github.com/johnfercher/maroto/pkg/pdf"
)

func main() {
    m := pdf.NewMaroto(consts.Portrait, consts.A4)
    m.SetPageMargins(20, 10, 20)

    err := m.OutputFileAndClose("pdfs/div_rhino_fruit.pdf")
    if err != nil {
        fmt.Println("⚠️  Could not save PDF:", err)
        os.Exit(1)
    }

    fmt.Println("PDF saved successfully")
}
Enter fullscreen mode Exit fullscreen mode

If we run our program now, we'll get an empty PDF file saved to our pdfs folder

go run main.go
Enter fullscreen mode Exit fullscreen mode

Building the header

An empty PDF file isn't all that interesting, so let's add some content, starting with a header. Our header will hold an image of the "Div Rhino Fruit" logo.

We don't want to put everything in our func main(), so let's create a new function to build our header. We will pass in our previously-created instance of pdf.Maroto as an argument. This function will perform some transformations on the pdf.Maroto instance (i.e. m), and we won't be returning any values.

func buildHeading(m pdf.Maroto) {}
Enter fullscreen mode Exit fullscreen mode

The Maroto package gives us a method that lets us register a header "component" that will appear on every page of our PDF document. This RegisterHeader() method accepts an anonymous callback function as the only argument. This anonymous function can be thought of as a header container.

func buildHeading(m pdf.Maroto) {
    m.RegisterHeader(func() {})
}
Enter fullscreen mode Exit fullscreen mode

Within the body of the "header container", we can set up a row that contains a column. Much like the Bootstrap grid system, rows are wrappers for columns. Here we've given our row a height of 50 and we've indicated that we want a full-width column that takes up 12 spaces. The number 12 is significant because most grid systems use 12 columns.

func buildHeading(m pdf.Maroto) {
    m.RegisterHeader(func() {
        m.Row(50, func() {
            m.Col(12, func() {

            })
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

We have a logo that was created beforehand and we've named it logo_div_rhino.jpg. To keep things organised, let's make a new images directory to hold images we use in this project. Feel free to use your own logo image instead.

mkdir images
Enter fullscreen mode Exit fullscreen mode

We will also want to import the props subpackage from maroto

import (
    "fmt"
    "os"

    "github.com/johnfercher/maroto/pkg/consts"
    "github.com/johnfercher/maroto/pkg/pdf"
    "github.com/johnfercher/maroto/pkg/props"
)
Enter fullscreen mode Exit fullscreen mode

Inside our full-width column, we can set up an image component to display our "Div Rhino Fruit" logo. We will centralise it and tell it to take up 75% of the height of the cell.

func buildHeading(m pdf.Maroto) {
    m.RegisterHeader(func() {
        m.Row(50, func() {
            m.Col(12, func() {
                m.FileImage("images/logo_div_rhino.jpg", props.Rect{
                    Center:  true,
                    Percent: 75,
                })
            })
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

The FileImage() method returns an error, so let's do some quick error handling before we move on. If the image cannot be loaded, we print a message in the console to let the user know.

func buildHeading(m pdf.Maroto) {
    m.RegisterHeader(func() {
        m.Row(50, func() {
            m.Col(12, func() {
                err := m.FileImage("images/logo_div_rhino.jpg", props.Rect{
                    Center:  true,
                    Percent: 75,
                })

                if err != nil {
                    fmt.Println("Image file was not loaded 😱 - ", err)
                }
            })
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

Next we want to create another row and full-width column to add some descriptive Text — "Prepared for you by the Div Rhino Fruit Company".

func buildHeading(m pdf.Maroto) {
    m.RegisterHeader(func() {
        m.Row(50, func() {
            m.Col(12, func() {
                err := m.FileImage("images/logo_div_rhino.jpg", props.Rect{
                    Center:  true,
                    Percent: 75,
                })

                if err != nil {
                    fmt.Println("Image file was not loaded 😱 - ", err)
                }

            })
        })
    })

    m.Row(10, func() {
        m.Col(12, func() {
            m.Text("Prepared for you by the Div Rhino Fruit Company", props.Text{
                Top:   3,
                Style: consts.Bold,
                Align: consts.Center,
                Color: getDarkPurpleColor(),
            })
        })
    })
}

func getDarkPurpleColor() color.Color {
    return color.Color{
        Red:   88,
        Green: 80,
        Blue:  99,
    }
}
Enter fullscreen mode Exit fullscreen mode

We want to use a custom colour here, so we will also want to make a new function func getDarkPurpleColor() and use the color sub-package that Maroto provides to create a dark purple colour

import (
    "fmt"
    "os"

    "github.com/johnfercher/maroto/pkg/consts"
    "github.com/johnfercher/maroto/pkg/pdf"
    "github.com/johnfercher/maroto/pkg/props"
    "github.com/johnfercher/maroto/pkg/color"
)
Enter fullscreen mode Exit fullscreen mode

And that's our header. Let's call it inside out func main()

func main() {
    m := pdf.NewMaroto(consts.Portrait, consts.A4)
    m.SetPageMargins(20, 10, 20)

    buildHeading(m)

    err := m.OutputFileAndClose("pdfs/div_rhino_fruit.pdf")
    if err != nil {
        fmt.Println("⚠️  Could not save PDF:", err)
        os.Exit(1)
    }

    fmt.Println("PDF saved successfully")
}
Enter fullscreen mode Exit fullscreen mode

We can run our code to generate a PDF file to see what this looks like:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Lay out a table of products

We can build our table of fruit next. We first want to give the whole table a heading. We can create a new teal colour and set the background colour of the cell to this teal colour. Then, like we've done previously, we can add a row that contains a full-width column. We are giving the Text component properties such as top position, size, color, etc.

func buildFruitList(m pdf.Maroto) {

    m.SetBackgroundColor(getTealColor())

    m.Row(10, func() {
        m.Col(12, func() {
            m.Text("Products", props.Text{
                Top:    2,
                Size:   13,
                Color:  color.NewWhite(),
                Family: consts.Courier,
                Style:  consts.Bold,
                Align:  consts.Center,
            })
        })
    })
}

func getTealColor() color.Color {
    return color.Color{
        Red:   3,
        Green: 166,
        Blue:  166,
    }
}
Enter fullscreen mode Exit fullscreen mode

At this point, if you'd like to see what this looks like, you can just generate a PDF and preview it:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Along with content, our table should have column headings as well. Similar to the HTML table structure, we first want to create something that resembles a thead and tbody. We also want to set the cell colour of this section to white and set the background colour of alternate rows to a light purple. We will need to create a new light purple colour function.

We can use the TableList() component that Maroto provides to get a table. We want both the HeaderProps and the ContentProps to have 3 columns. We can do this by giving the GridSizes property a value of []uint{3, 7, 2} (a slice of unsigned integers with the values of 3, 7 and 2). These grid sizes add up to 12.

func buildFruitList(m pdf.Maroto) {
    tableHeadings := []string{"Fruit", "Description", "Price"}
    contents := [][]string{{"Apple", "Red and juicy", "2.00"}, {"Orange", "Orange and juicy", "3.00"}}
    lightPurpleColor := getLightPurpleColor()

    m.SetBackgroundColor(getTealColor())
    m.Row(10, func() {
        m.Col(12, func() {
            m.Text("Products", props.Text{
                Top:    2,
                Size:   13,
                Color:  color.NewWhite(),
                Family: consts.Courier,
                Style:  consts.Bold,
                Align:  consts.Center,
            })
        })
    })

    m.SetBackgroundColor(color.NewWhite())

    m.TableList(tableHeadings, contents, props.TableList{
        HeaderProp: props.TableListContent{
            Size:      9,
            GridSizes: []uint{3, 7, 2},
        },
        ContentProp: props.TableListContent{
            Size:      8,
            GridSizes: []uint{3, 7, 2},
        },
        Align:                consts.Left,
        AlternatedBackground: &lightPurpleColor,
        HeaderContentSpace:   1,
        Line:                 false,
    })

}

...

func getLightPurpleColor() color.Color {
    return color.Color{
        Red:   210,
        Green: 200,
        Blue:  230,
    }
}
Enter fullscreen mode Exit fullscreen mode

Just some notes on the code, above:

  • the tableHeadings if of type slice of string
  • the contents are a slice of slice of string. Another way of saying slice of slice of string is to say two-dimensional slice. This type will be important in the next section when we work on our mock data generator.

Call the buildFruitList() in func main()

func main() {
    m := pdf.NewMaroto(consts.Portrait, consts.A4)
    m.SetPageMargins(20, 10, 20)

    buildHeading(m)
    buildFruitList(m)


    err := m.OutputFileAndClose("pdfs/div_rhino_fruit.pdf")
    if err != nil {
        fmt.Println("⚠️  Could not save PDF:", err)
        os.Exit(1)
    }

    fmt.Println("PDF saved successfully")
}
Enter fullscreen mode Exit fullscreen mode

Let's do a quick preview of our PDF again to see what we've just added so far

go run main.go
Enter fullscreen mode Exit fullscreen mode

Mock data generator

Okay, now let's take a small detour out of our main.go file. So far we've used hardcoded content to inject into our table. We're going to try something different and use some randomly-generated data instead. To achieve this, we will create our own custom data package and make use of the GoFakeIt package we installed, earlier.

In our project root, let's make a new folder called data

mkdir data
Enter fullscreen mode Exit fullscreen mode

And within this new data folder, let's create a new file called products.go

touch data/products.go
Enter fullscreen mode Exit fullscreen mode

Let's head into our newly-created products.go file. This is going to be a new package on its own, so we can start by indicating that it's part of package data instead of package main. Then we can import GoFakeIt.

package data

import "github.com/brianvoe/gofakeit/v6"
Enter fullscreen mode Exit fullscreen mode

The GoFakeIt packages gives us all sorts of functions for concepts such as a File, a Person, a Number, among other things. We will be using Fruit in this tutorial.

We can represent the structure of each fruit item using a Fruit struct type. Each Fruit will have a Name, a Description and a Price. Each of these values will be randomly generated using GoFakeIt.

package data

import "github.com/brianvoe/gofakeit/v6"

type Fruit struct {
    Name        string  `fake:"{fruit}"`
    Description string  `fake:"{loremipsumsentence:10}"`
    Price       float64 `fake:"{price:1,10}"`
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our Fruit struct type, we can create a function to make use of it. Every time the generateFruit() function is called, we get a new random fruit.

package data

import (
    "fmt"

    "github.com/brianvoe/gofakeit/v6"
)

type Fruit struct {
    Name        string  `fake:"{fruit}"`
    Description string  `fake:"{loremipsumsentence:10}"`
    Price       float64 `fake:"{price:1,10}"`
}

func generateFruit() []string {
    var f Fruit
    gofakeit.Struct(&f)

    froot := []string{}
    froot = append(froot, f.Name)
    froot = append(froot, f.Description)
    froot = append(froot, fmt.Sprintf("%.2f", f.Price))

    return froot
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we want to create a function we can access outside this data package. We need this in order to generate this random fruit data inside out PDF table that lives in our main.go file.

package data

import (
    "fmt"

    "github.com/brianvoe/gofakeit/v6"
)

type Fruit struct {
    Name        string  `fake:"{fruit}"`
    Description string  `fake:"{loremipsumsentence:10}"`
    Price       float64 `fake:"{price:1,10}"`
}

func generateFruit() []string {
    var f Fruit
    gofakeit.Struct(&f)

    froot := []string{}
    froot = append(froot, f.Name)
    froot = append(froot, f.Description)
    froot = append(froot, fmt.Sprintf("%.2f", f.Price))

    return froot
}

func FruitList(length int) [][]string {
    var fruits [][]string

    for i := 0; i < length; i++ {
        onefruit := generateFruit()
        fruits = append(fruits, onefruit)
    }

    return fruits
}
Enter fullscreen mode Exit fullscreen mode

Just some notes about the code, above:

  • FruitList() is a public function that we can access from outside the data package, which is why it starts with a capital letter.
  • As we saw earlier, our TableList component needs the data to come in as a two-dimensional slice of string.
  • FruitList() takes in one parameter of length so we can dynamically determine how many items of fruit we want to generate.
  • We then pass this length value into a little for loop that calls our generateFruit() function however many times length determines.
  • Then we return a two-dimensional slice of string.

Hooking up the dynamic content

Back in our main.go file, we can import our data package and replace our contents variable. Let's use the FruitList() function to generate 20 random fruit.

package main

import (
    "fmt"
    "os"

    "github.com/johnfercher/maroto/pkg/color"
    "github.com/johnfercher/maroto/pkg/consts"
    "github.com/johnfercher/maroto/pkg/pdf"
    "github.com/johnfercher/maroto/pkg/props"

    "github.com/divrhino/fruitful-pdf/data"
)

...

func buildFruitList(m pdf.Maroto) {
    headings := []string{"Fruit", "Description", "Price"}
    contents := data.FruitList(20)

...
}

Enter fullscreen mode Exit fullscreen mode

And now if we run our code, we have a table of 20 randomly-generated fruit.

go run main.go
Enter fullscreen mode Exit fullscreen mode

If we decide to generate many, many more random Fruit, say 100, our PDF automatically adds pages to accommodate this.

Going further

If you'd like to extend this project further, you can consider doing the following

  • try replacing the mock data with actual data from an API
  • use the RegisterFooter() method to add a footer to every page of the PDF document
  • add a signature, barcode, page numbers and a QR code

This repository includes an extended version of this tutorial code in the examples folder. You can also look at the Maroto package Github page for even more examples.

Conclusion

In this tutorial we learnt how to generate some dummy data that we saved in a PDF file. We used the maroto and gofakeit packages to help us achieve this.

If you enjoyed this article and you'd like more, consider following Div Rhino on YouTube.

Congratulations, you did great! Keep learning and keep coding. Bye for now, <3

GitHub logo divrhino / fruitful-pdf

Create a PDF document with Go, Maroto and GoFakeIt. Video tutorial available on the Div Rhino YouTube channel.

Discussion (0)

Forem Open with the Forem app