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.
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.
Related reading
- gofpdf is archived. Here's how to migrate to gpdf. — if you're coming from
pdf.AddUTF8Fontand want the full migration map - Go PDF Library Showdown 2026 — how gpdf compares to gofpdf, gopdf, Maroto, and unipdf on CJK
- Fonts guide — the full
WithFontreference including variant naming rules
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