All posts

How do I embed a Japanese font in gpdf?

Register a Japanese TrueType font with gpdf.WithFont at document construction. Three lines, subset embedding happens automatically, no CGO.

by gpdf team

The question, in other words

How do I render Japanese (or any CJK) text in a PDF generated with gpdf — without the AddUTF8Font dance, without CGO, without a five-megabyte font embedded on every document?

TL;DR

Load the TTF bytes. Pass gpdf.WithFont("NotoSansJP", fontBytes) to NewDocument. Optionally set it as the default. Write Japanese. Three lines of setup, and gpdf subsets the glyphs automatically so the final PDF carries only the characters you actually used.

The complete example

package main

import (
    "log"
    "os"

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

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

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("NotoSansJP", font),
        gpdf.WithDefaultFont("NotoSansJP", 12),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("こんにちは、世界。", template.FontSize(24), template.Bold())
            c.Text("日本語 PDF、これだけ。")
        })
    })

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

Download NotoSansJP-Regular.ttf from Google Fonts and drop it next to main.go. Run go run main.go. You get a one-page PDF with Japanese text.

What those three lines actually do

Two things happen under the hood, and neither one needs any help from you.

Subset embedding. Noto Sans JP ships with around 17,000 glyphs — the regular weight is about 5 MB on disk. If every PDF embedded the whole font, a receipt with four lines of Japanese would still cost you five megabytes of font data. gpdf walks the text you rendered, figures out which glyph IDs you used, and writes only that subset into the PDF. A short invoice usually ends up carrying 20–40 KB of font data instead of 5 MB.

gofpdf could do subset embedding too, but it needed AddUTF8Font called with a filesystem path and a UTF-8 flag, and font switching mid-document was awkward. gpdf registers the font once at document construction and then every c.Text call just references it by family name. There's no per-call bookkeeping.

No CGO. This matters more than it sounds. A lot of font handling in other ecosystems routes through FreeType or HarfBuzz, which means a C dependency, which means your build caches invalidate differently, your Docker images gain layers, and cross-compilation from macOS to linux/arm64 becomes a thing you have to think about. gpdf parses TrueType tables in pure Go. go build stays static. Ship a distroless container with the Go binary and the TTF file; that's all.

Bold and italic variants

Japanese Noto ships a separate file per weight. To use bold, register the bold TTF under the -Bold suffix:

reg, _ := os.ReadFile("NotoSansJP-Regular.ttf")
bold, _ := os.ReadFile("NotoSansJP-Bold.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", reg),
    gpdf.WithFont("NotoSansJP-Bold", bold),
    gpdf.WithDefaultFont("NotoSansJP", 12),
)

Now template.Bold() picks up the -Bold variant. Same convention for -Italic and -BoldItalic. If you don't register the variant, bold falls back to a synthesized weight — readable on screen but not typographically honest. For production invoices, register the real weight.

Multiple CJK languages in the same document

Registering more than one family is fine — gpdf tracks them independently. Use template.FontFamily(...) to switch per text:

jp, _ := os.ReadFile("NotoSansJP-Regular.ttf")
sc, _ := os.ReadFile("NotoSansSC-Regular.ttf")
kr, _ := os.ReadFile("NotoSansKR-Regular.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", jp),
    gpdf.WithFont("NotoSansSC", sc),
    gpdf.WithFont("NotoSansKR", kr),
    gpdf.WithDefaultFont("NotoSansJP", 12),
)

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("日本語")
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("中文", template.FontFamily("NotoSansSC"))
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("한국어", template.FontFamily("NotoSansKR"))
    })
})

Han unification means the Unicode codepoints between Japanese and Simplified Chinese overlap, but the glyphs are drawn differently. Picking the right font isn't just aesthetic — the same codepoint renders as a different character shape depending on the font. If you're producing invoices for both markets, get both files registered.

The tofu trap

If you write Japanese but forget WithFont, gpdf falls back to the Base-14 PDF fonts — none of which cover the CJK range. The characters render as blank rectangles, what Unicode people call "tofu boxes":

□□□□□、□□。

If you see that output, you forgot to register a CJK font, or you wrote the text in a font family that doesn't include those glyphs. The fix is always the same: add WithFont and either use WithDefaultFont or pass template.FontFamily on the c.Text call.

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