All posts

How do I use Noto Sans JP with gpdf?

Register the static NotoSansJP-Regular.ttf with gpdf.WithFont. Skip the variable font — gpdf's pure-Go parser does not read fvar tables. Subsetting lands around 30 KB.

by gpdf team

The question, in other words

You want Japanese text in a gpdf document, you've picked Noto Sans JP — Google's free, SIL-OFL-licensed sans-serif that covers the full JIS range — and you want to know the three things nobody spells out: which file to download, which weights to register, and the one footgun hiding in the zip.

TL;DR

Use the static NotoSansJP-Regular.ttf from the static/ folder inside the Google Fonts zip. Not the variable font at the root. Pass it to gpdf.WithFont("NotoSansJP", bytes) and set it as the default. gpdf subsets the ~17,000 glyphs down to whatever you rendered — an invoice typically carries 20–40 KB of font data in the final PDF.

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", 11),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("請求書", template.FontSize(28), template.Bold())
            c.Text("Noto Sans JP、これで十分。")
        })
    })

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

Download the Noto Sans JP zip from Google Fonts, extract static/NotoSansJP-Regular.ttf, drop it next to main.go, and run go run main.go.

Grab the static TTF, not the variable font

Open the Google Fonts page, hit Get fontDownload all, unzip. You get two things that look interchangeable and aren't:

  • NotoSansJP-VariableFont_wght.ttf at the root — the variable font, ~7 MB, carries every weight between 100 and 900 in a single file
  • static/ — nine separate TTFs, NotoSansJP-Thin.ttf through NotoSansJP-Black.ttf, each ~5 MB

Pick from static/.

gpdf's TrueType parser is deliberately scoped. It handles glyph outlines, composite glyphs, cmap, and hmtx — the tables you need to render fixed-weight type. It does not evaluate fvar, gvar, or HVAR, which are the OpenType tables that make variable fonts actually variable. Hand it the VariableFont_wght.ttf and the parser either errors out or falls through to the default instance glyphs, silently ignoring any weight axis you thought you were setting.

The file-size math also argues against it. A variable font bundles every weight's outlines — that's the whole point. If you use only Regular, you're paying for eight weights of outline data that never render. Static Regular is 5 MB; the variable font is 7 MB. Subsetting will prune both down, but the static file is cleaner input.

The four lines that matter

Everything interesting is in the constructor options:

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", font),
    gpdf.WithDefaultFont("NotoSansJP", 11),
)

The family name ("NotoSansJP") is arbitrary. gpdf uses it as a lookup key — not a filesystem path, not a field read from the font's metadata. Call it "body" or "jp" or "Noto" if that reads better in your codebase. Just keep it consistent with the argument you pass to template.FontFamily(...) later.

WithDefaultFont is the one that saves you from writing template.FontFamily("NotoSansJP") on every single c.Text call. Skip it and gpdf falls back to Helvetica for unlabeled text, and Helvetica covers zero CJK codepoints. You'll get tofu boxes on half the document and spend an hour figuring out why only the headings render.

Which weights do you actually need?

Most invoices, receipts, and reports need two: Regular and Bold. Register both:

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", 11),
)

With -Bold registered under that suffix, template.Bold() picks it up automatically. Same rule for -Italic and -BoldItalic. Note that Noto Sans JP doesn't ship an italic — CJK fonts generally don't, because the glyph design doesn't have an obvious oblique. If your layout wants italic emphasis on a Japanese run, reach for color, size, or weight instead.

Marketing brochures occasionally want Medium or SemiBold for pull quotes. That's fine. Register them under any suffix and address them by family name directly:

gpdf.WithFont("NotoSansJP-Medium", medium)
// ...
c.Text("見出し", template.FontFamily("NotoSansJP-Medium"))

The suffix-based Bold/Italic shortcut only hooks up for the literal -Bold / -Italic / -BoldItalic names. Anything else, you reference by family name.

The size after subsetting

Noto Sans JP Regular is ~5 MB on disk. That number pushes people into building separate CDNs for font files or post-processing PDFs to strip embedded fonts. Neither is necessary with gpdf.

Here's what actually lands in the PDF:

DocumentGlyphs usedFont data in PDF
One-line receipt (~15 chars)~14~11 KB
Typical invoice (~200 chars)~80~28 KB
10-page report (~8,000 chars)~900~180 KB
Dictionary-style dump (full JIS Level 1)~6,800~2.1 MB

(gpdf v1.0, static subsetting on. Numbers shift by a few KB depending on which glyph IDs fall in the CFF and hmtx tables.)

For an invoice that ends up 50 KB, more than half of the bytes are font data. That's still a rounding error next to the 5 MB you'd embed without subsetting, and the PDF viewer opens it instantly.

Noto Sans JP vs Noto Sans CJK JP — don't mix these up

There are two Noto families that both claim to handle Japanese, and the naming makes them sound interchangeable. They aren't.

Noto Sans JP is the one you want. Distributed as TTF, one language, each weight is a separate file. This is the Google Fonts download.

Noto Sans CJK JP is the pan-CJK super-family. Distributed as OpenType Collection (.ttc), a single file containing Japanese, Simplified Chinese, Traditional Chinese, and Korean glyphs unified in one package. It's what shipped in earlier Noto releases and what you'll find on notofonts.github.io/noto-cjk.

gpdf supports TTF directly. TTC is a container format — you'd need to pick the right face index before handing bytes to WithFont, and the cmap inside each face is tuned for a specific CJK locale, which means you're making implicit choices about Han unification. Easier to make those choices explicitly by picking the JP-specific TTF.

Starting today? Use Noto Sans JP. Already have NotoSansCJK-Regular.ttc in a legacy project? Extract the JP face with pyftsubset or fonttools and check the resulting TTF into your repo as the canonical artifact.

Embedding the font into the binary

PDF generators usually run in containers, and the cleanest way to ship the font is to compile it in:

package main

import (
    _ "embed"

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

//go:embed NotoSansJP-Regular.ttf
var notoJP []byte

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithFont("NotoSansJP", notoJP),
        gpdf.WithDefaultFont("NotoSansJP", 11),
    )
    // ...
}

Binary grows from ~8 MB to ~13 MB. In return, your Docker image has one artifact instead of two, COPY --from=builder /app /app is all you need, and nobody can ship a broken container missing the font file. For a batch job generating thousands of PDFs a day, this is the right default.

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