All posts

How do I set a custom paper size in gpdf?

Build a document.Size in points — document.Mm(100).Resolve(0, 0) converts — and pass it to WithPageSize. Labels, receipts, JIS B5, and the traps.

Posted on November 10, 2026
by Taiki Noda

The question, in other words

The page you need isn't A4 or Letter. It's a 4×6 inch shipping label, an 80 mm thermal receipt, a 100×148 mm postcard, or JIS B5 — which, annoyingly, isn't the same as the document.B5 constant. WithPageSize takes a document.Size. How do you build one with arbitrary dimensions, and what goes wrong when you do?

TL;DR

document.Size is two float64 fields in PDF points (1/72 inch). Convert with Resolve, then pass it in:

label := document.Size{
	Width:  document.In(4).Resolve(0, 0), // 288 pt
	Height: document.In(6).Resolve(0, 0), // 432 pt
}
doc := template.New(template.WithPageSize(label))

That's it for the happy path. The rest of this post is the three ways it goes sideways.

The code

A complete 4×6 inch shipping label. Verified against gpdf v1.0.17 — the output's /MediaBox is [0 0 288 432].

package main

import (
	"log"
	"os"

	"github.com/gpdf-dev/gpdf/document"
	"github.com/gpdf-dev/gpdf/template"
)

// mm converts millimetres to PDF points, the unit document.Size uses.
func mm(v float64) float64 { return document.Mm(v).Resolve(0, 0) }

// in converts inches to PDF points.
func in(v float64) float64 { return document.In(v).Resolve(0, 0) }

func main() {
	// A 4×6 inch shipping label: 288 × 432 pt.
	label := document.Size{Width: in(4), Height: in(6)}

	doc := template.New(
		template.WithPageSize(label),
		template.WithMargins(document.UniformEdges(document.Mm(6))),
	)

	page := doc.AddPage()
	page.AutoRow(func(r *template.RowBuilder) {
		r.Col(12, func(c *template.ColBuilder) {
			c.Text("SHIP TO", template.Bold(), template.FontSize(10))
			c.Text("Jane Doe", template.FontSize(18), template.Bold())
			c.Text("742 Evergreen Terrace")
			c.Text("Springfield, OR 97477")
			c.Spacer(document.Mm(6))
			c.QRCode("TRACK-1Z999AA10123456784", template.QRSize(document.Mm(40)))
		})
	})

	data, err := doc.Generate()
	if err != nil {
		log.Fatal(err)
	}
	if err := os.WriteFile("label.pdf", data, 0o644); err != nil {
		log.Fatal(err)
	}
}

The two little mm / in helpers are worth copying into your project. You'll write them anyway.

Why Size isn't a Value

Everywhere else in gpdf you pass document.Mm(20) and the layout engine figures out points later. Margins, spacers, QR sizes — all document.Value. So it's natural to try this:

document.Size{Width: document.Mm(100), Height: document.Mm(150)} // doesn't compile

It doesn't compile, because Size is defined as:

type Size struct {
	Width, Height float64
}

The reason is that Size is the bottom of the coordinate system. A Value can be Pct or Em, and those need a parent size or a font size to resolve. The page is the parent. There is nothing above it to be a percentage of. So the page has to be concrete numbers, and the numbers are points because that's what a PDF /MediaBox is written in.

Resolve(parentSize, fontSize) is the method the layout engine calls internally. For absolute units both arguments are ignored, so Resolve(0, 0) is just "give me points". The factors are fixed: Mm is ×2.83465, Cm ×28.3465, In ×72.

Now the trap that comes with it. This does compile:

document.Size{Width: 100, Height: 150} // you meant mm

and gives you a 35 × 53 mm page. No error, no warning — 100 pt is a perfectly valid width. You find out when the PDF opens and the label looks like a postage stamp. If you take one thing from this post: never write a bare number into a Size unless you're thinking in points.

Orientation and the JIS B5 problem

Width comes first, height second. For landscape, either swap them yourself or call Landscape():

template.WithPageSize(document.A4.Landscape())

Landscape() only swaps if height is greater than width, so calling it on an already-landscape size is a no-op. Handy when the size comes from config and you don't know which way round it is.

A side note for anyone printing in Japan. gpdf's document.B5 is ISO B5, 176 × 250 mm. The B5 your office copier means is JIS B5, 182 × 257 mm. The constants' GoDoc says so, but nobody reads GoDoc until the print comes out with a 3 mm gap. JIS sizes are custom sizes:

jisB5 := document.Size{Width: mm(182), Height: mm(257)}
jisB4 := document.Size{Width: mm(257), Height: mm(364)}

One size per document

There's no AddPage(size). WithPageSize is a document option, and every page — including overflow pages the paginator creates on its own — uses it.

If you need an A4 cover letter followed by a 4×6 label in one file, generate them separately and merge:

merged, err := gpdf.Merge([]gpdf.Source{
	{Data: coverA4},  // generated with document.A4
	{Data: labelPDF}, // generated with the 4×6 size
})

Each page keeps its own /MediaBox through the merge — we checked: 595.28 × 841.89 then 288 × 432. Print dialogs handle mixed sizes fine. Some label printers' drivers do not, but that's between you and the driver.

JSON schema: names only

The JSON schema's page.size accepts named sizes — "A0"–"A10", "B0"–"B10", "C0"–"C10", "DL", "Letter", "Legal" — plus "orientation": "landscape". A string like "100mm x 150mm" fails with unknown page size: "100mm x 150mm".

Options passed from Go override the schema, though. So keep the template in JSON and set the size in code:

doc, err := gpdf.NewDocumentFromJSON(schema, data,
	gpdf.WithPageSize(document.Size{Width: mm(80), Height: mm(200)}),
)

That works for NewDocumentFromTemplate too.

Small pages and the 20 mm margin

This one bit us while writing this post, so it gets its own section.

template.New defaults to 20 mm margins on all four sides. On A4 that's sensible. On a 50 × 30 mm product label, 40 mm of vertical margin leaves −10 mm of content height. On a 60 × 40 point page (the bare-number trap again) with 5 mm margins, you get about 12 pt of content height — less than one line of 12 pt text.

In both cases, up to v1.0.16, Generate() didn't return an error. It just didn't return. The paginator kept trying to place a row that would never fit. We had three test programs hang before we worked out the common factor, and honestly that was a stupid half hour. Then, while taking screenshots for the fix, a fourth case still hung: a word too wide for the column was broken mid-word, and the leftover text quietly gained a space on every page, so it never looked stuck. That one we did not see coming.

Since v1.0.17 it fails in microseconds instead:

import "github.com/gpdf-dev/gpdf/document/layout"

data, err := doc.Generate()
if errors.Is(err, layout.ErrContentDoesNotFit) {
	// layout: content does not fit on an empty page: body area is
	// 28.35 x 0.00 pt on a 141.73 x 85.04 pt page; ...
	log.Fatal(err)
}

The body-area numbers are usually enough to spot the mistake. A height of zero or less means the margins, header or footer ate the whole page.

So: on anything smaller than A6, still set margins explicitly. The error tells you something is wrong; it doesn't pick margins for you.

Receipts are the friendlier case. An 80 mm wide page with a fixed height simply paginates — 40 rows of items on an 80 × 120 mm page gave us three pages. If you want one continuous strip instead, you have to pick a height up front; gpdf doesn't grow the page to fit content.

Try gpdf

gpdf is a Go library for generating PDFs. MIT licensed, zero external dependencies, native CJK support.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs