All posts

How do I mix two fonts in the same paragraph in gpdf?

To mix fonts in one paragraph in gpdf, call c.RichText and set template.FontFamily on each span — c.Text only styles the whole string at once.

The question, in other words

I have one paragraph — a sentence, a label, a table cell — and I want part of it in one font and part of it in another. A code snippet in monospace inside a line of Helvetica. A Japanese name in Noto Sans JP next to an ASCII order ID. How do I switch fonts mid-paragraph without breaking the text into separate blocks?

The quick answer

c.Text is the wrong tool here. It applies one document.Style — one font family included — to the whole string. The tool you want is c.RichText, where every span carries its own style:

c.RichText(func(rt *template.RichTextBuilder) {
    rt.Span("Run ")
    rt.Span("gofmt ./...", template.FontFamily("Courier"))
    rt.Span(" before you commit.")
})

Three spans, two fonts, one paragraph. The layout engine line-breaks across the span boundaries the way a word processor would, so the monospace fragment flows inline with the Helvetica around it.

Courier works with no WithFont call because it's one of the PDF Standard 14 fonts — every viewer already has it, same as Helvetica and Times-Roman. If your second font is a TrueType file you supply (a brand font, a CJK font), you register it once and refer to it by name. More on that below.

Working code (Helvetica + Courier, no font files)

package main

import (
    "log"
    "os"

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

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.RichText(func(rt *template.RichTextBuilder) {
                rt.Span("Run ")
                rt.Span("gofmt ./...", template.FontFamily("Courier"))
                rt.Span(" before every commit. ")
                rt.Span("It is not optional", template.Bold(), template.Italic())
                rt.Span(".")
            })
            c.RichText(func(rt *template.RichTextBuilder) {
                rt.Span("The field is ")
                rt.Span("created_at", template.FontFamily("Courier"), template.TextColor(pdf.RGBHex(0xB00020)))
                rt.Span(" — not ")
                rt.Span("createdAt", template.FontFamily("Courier"))
                rt.Span(".")
            })
        })
    })

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

The body text stays in Helvetica (the default), the inline identifiers switch to Courier, and one span layers bold + italic on top of the default font. No WithFont, no embedded font data — the PDF references Helvetica, Helvetica-BoldOblique, and Courier as non-embedded Type 1 entries that every reader already has.

What RichText does with the spans

Each rt.Span becomes a document.RichTextFragment with its own copy of the style. A span you call with no options inherits the block style — which for RichText is the column's default, i.e. the document's default font and size. A span you call with template.FontFamily("Courier") gets exactly that field overridden and keeps everything else.

At layout time gpdf splits every fragment into word-level runs, measures each run with that run's own font metrics — this is why a Courier word and a Helvetica word on the same line get the right widths — and then greedily packs runs into lines. All runs on a line share one baseline, so a 24 pt span sitting next to a 12 pt span lines up at the bottom and the line's height grows to fit the tall one.

One distinction trips people up: the second argument to c.RichText is paragraph-level style, the per-span options are fragment-level.

OptionWhere it belongs
FontFamily, FontSize, Bold, Italic, TextColor, Underline, Strikethroughper-span — pass to each rt.Span
AlignLeft / AlignCenter / AlignRight / AlignJustify, line height, TextIndentparagraph-level — pass as the second arg to c.RichText

Putting AlignRight() on an individual rt.Span does nothing; alignment is a property of the line, not the fragment.

The real case: a Latin font next to a CJK font

Monospace-inside-a-sentence is the easy version. The one people actually fight with is mixing a Western font with a CJK font on one line — an English label and a Japanese value, a product code and a 商品名. Two things to know.

First, gpdf does not pick a font by script. If a span's family is Helvetica and the text is 日本語, you get tofu boxes — Helvetica has no CJK glyphs, and gpdf will not silently reach for some other registered font to cover them. Put the CJK family on the CJK span yourself:

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

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", ttf),
)
// ...
c.RichText(func(rt *template.RichTextBuilder) {
    rt.Span("Customer: ")                                 // default → Helvetica
    rt.Span("山田 太郎", template.FontFamily("NotoSansJP")) // CJK → Noto Sans JP
    rt.Span("  (ID 10293)")                               // back to Helvetica
})

Second — and this is the part worth saying out loud — most Japanese CJK fonts already carry decent Latin glyphs. Noto Sans JP, IPAex, Source Han Sans: all of them draw ID 10293 perfectly well. So before you reach for a per-span mix, ask whether you actually want two fonts or just got there by habit. If the whole document is Japanese-with-some-ASCII, the simplest thing is gpdf.WithDefaultFont("NotoSansJP", 11) and never mix at all. Reach for RichText + FontFamily when you genuinely want a different look — a clean geometric Latin face for the numbers, a humanist CJK face for the prose — not just to make the script render.

When c.Text is still fine

If the whole string is one font, keep using c.Text — it's lighter and reads better. c.Text("発行日: 2026-05-11", template.FontFamily("NotoSansJP")) is one font for the whole line, and c.Text handles it. RichText earns its keep only when the style changes inside the string. Don't wrap a single-style line in a RichText callback just because you can.

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