All posts

How do I add a custom TrueType font to gpdf?

Load TTF bytes, register with gpdf.WithFont, then reference the family name. Works for any TrueType — Inter, Roboto, icon fonts, brand fonts.

The question, in other words

I have a .ttf file — Inter for the brand, JetBrains Mono for code blocks, an icon font for glyphs. How do I get it into a gpdf document and reference it from a c.Text(...) call?

TL;DR

Load the TTF bytes. Pass gpdf.WithFont("YourFamily", bytes) to NewDocument. Then reference "YourFamily" from template.FontFamily(...) or set it as the default with gpdf.WithDefaultFont.

The family name is arbitrary. It has nothing to do with the font's internal name table — it's just the lookup key gpdf uses when resolving a FontFamily option. Pick something short.

Working code

package main

import (
    "log"
    "os"

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

func main() {
    regular, err := os.ReadFile("Inter-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("Inter", regular),
        gpdf.WithDefaultFont("Inter", 11),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("Quarterly Report", template.FontSize(28))
            c.Text("Generated with gpdf and Inter.")
        })
    })

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

Drop Inter-Regular.ttf next to main.go (download from rsms.me/inter). go run main.go. Done.

What gpdf does with the bytes

When Generate() runs, gpdf parses the TrueType tables (cmap, glyf, loca, hmtx, …) in pure Go — no FreeType, no CGO. It walks the rendered text, collects the code points actually used, and subsets the glyph table to that set. The PDF embeds a Type0 / CIDFontType2 font carrying only the glyphs you needed.

Practical effect: a 600 KB Inter-Regular.ttf becomes roughly a 12 KB font subset inside the PDF if your document used a couple of paragraphs. The brand font lands without bloating the file.

Bold and italic need their own files

This is the part that bites people. gpdf does not synthesize bold or italic — there is no algorithmic "make it bolder" step. It looks up a variant ID built from the style flags:

Bold()Italic()Lookup key
nonoInter
yesnoInter-Bold
noyesInter-Italic
yesyesInter-BoldItalic

If you didn't register Inter-Bold, the lookup falls back to plain Inter — silently. The PDF renders, but everything stays regular weight. There's no warning.

Register all four:

regular, _    := os.ReadFile("Inter-Regular.ttf")
bold, _       := os.ReadFile("Inter-Bold.ttf")
italic, _     := os.ReadFile("Inter-Italic.ttf")
boldItalic, _ := os.ReadFile("Inter-BoldItalic.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("Inter", regular),
    gpdf.WithFont("Inter-Bold", bold),
    gpdf.WithFont("Inter-Italic", italic),
    gpdf.WithFont("Inter-BoldItalic", boldItalic),
    gpdf.WithDefaultFont("Inter", 11),
)

If a font ships only one weight (lots of icon and display fonts do), don't call template.Bold() or template.Italic() for those at all. Skipping a variant is fine. Falling back to the wrong variant is what produces "why is the bold not bold" bug reports.

Embed the font in the binary

os.ReadFile at startup works in development. In production the font is part of the program — it should travel inside the binary:

import _ "embed"

//go:embed fonts/Inter-Regular.ttf
var interRegular []byte

doc := gpdf.NewDocument(
    gpdf.WithFont("Inter", interRegular),
)

go build bakes the bytes in. No more "where is the .ttf in the deploy image" debugging on a Friday afternoon.

Icon fonts work the same way

Font Awesome, Material Symbols exported as TTF, IcoMoon, custom brand glyph sets — they're all just TrueType files. Register them the same way:

icons, _ := os.ReadFile("MaterialSymbols-Regular.ttf")
doc := gpdf.NewDocument(
    gpdf.WithFont("Icons", icons),
    gpdf.WithDefaultFont("Inter", 11), // body text default
)

// In a column:
c.Text("", template.FontFamily("Icons"), template.FontSize(20)) // "home" icon

The Unicode escape is whatever the font's documentation says it is. gpdf doesn't care that the glyph is an icon — to it, it's a code point, and it subsets the same way it does for letters.

Common mistakes

  • Family name typo at the call site. template.FontFamily("Intr") falls back to the document default. No error, no warning. If text suddenly looks like Helvetica, this is the first place to look.
  • Not embedding via //go:embed in containers. A trimmed Docker context drops the .ttf, the runtime fallback kicks in, and you find out from a customer email. Embed.
  • Using the font's PostScript name as the family. "Inter-Regular" is the PostScript name. Pass that to WithFont and the bold lookup tries to find "Inter-Regular-Bold" — which doesn't exist. Pick a clean family root ("Inter") and let the variant suffix handle the styles.

Try gpdf

gpdf is a Go library for generating PDFs. MIT licensed, zero external dependencies, pure-Go TrueType handling.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs