Web development articles for project-based learning

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
Image: 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

1
2
3
cd Sites
mkdir fruitful-pdf
cd fruitful-pdf

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

1
go mod init github.com/divrhino/fruitful-pdf

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.

1
2
go get -u github.com/johnfercher/maroto
go get github.com/johnfercher/maroto/internal@v0.31.0

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

1
go get github.com/brianvoe/gofakeit/v6

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.

1
touch main.go

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

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

func main() {}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

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

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

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

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

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

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

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

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

	m.OutputFileAndClose("pdfs/div_rhino_fruit.pdf")
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

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

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)
	}
}

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

1
mkdir pdfs

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

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

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")
}

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

1
go run main.go

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.

1
func buildHeading(m pdf.Maroto) {}

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.

1
2
3
func buildHeading(m pdf.Maroto) {
	m.RegisterHeader(func() {})
}

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.

1
2
3
4
5
6
7
8
9
func buildHeading(m pdf.Maroto) {
	m.RegisterHeader(func() {
		m.Row(50, func() {
			m.Col(12, func() {

			})
		})
	})
}

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.

1
mkdir images

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func buildHeading(m pdf.Maroto) {
	m.RegisterHeader(func() {
		m.Row(50, func() {
			m.Col(12, func() {
				m.FileImage("assets/images/logo_div_rhino.jpg", props.Rect{
					Center:  true,
					Percent: 75,
				})
			})
		})
	})
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func buildHeading(m pdf.Maroto) {
	m.RegisterHeader(func() {
		m.Row(50, func() {
			m.Col(12, func() {
				err := m.FileImage("assets/images/logo_div_rhino.jpg", props.Rect{
					Center:  true,
					Percent: 75,
				})

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

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

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func buildHeading(m pdf.Maroto) {
	m.RegisterHeader(func() {
		m.Row(50, func() {
			m.Col(12, func() {
				err := m.FileImage("assets/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,
	}
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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")
}

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

1
go run main.go

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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,
	}
}

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

1
go run main.go

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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,
	}
}

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.

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

1
go run main.go

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

1
mkdir data

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

1
touch data/products.go

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.

1
2
3
package data

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

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.

1
2
3
4
5
6
7
8
9
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}"`
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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++ {
		ff := generateFruit()
		fruits = append(fruits, ff)
	}

	return fruits
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)

...
}

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

1
go run main.go

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. You can find the finished code in the Github repository.

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

Resources