All posts

How do I use bold and italic together in gpdf?

Bold and italic on the same span work via template.Bold() and template.Italic(). For TrueType fonts you must register all four variants or the BoldItalic lookup silently falls back to the base family.

The question, in other words

I want a single word — or a whole line — to be both bold and italic in the same PDF. How do I set both at once, and why does it sometimes come out looking like neither?

The quick answer

Pass both options on the same c.Text call:

c.Text("WARNING", template.Bold(), template.Italic())

gpdf builds the variant ID Family-BoldItalic and looks it up in the registered fonts. For the Adobe Standard 14 families (Helvetica, Courier, Times) this just works — gpdf aliases -BoldItalic to the canonical -BoldOblique internally and uses the built-in AFM metrics. For a TrueType font you register yourself, you have to register all four variants or the lookup silently falls back to the base family.

That second point is where most of the bugs live.

Working code (Helvetica, no font registration)

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "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.Text("Regular Helvetica.")
            c.Text("Bold only.", template.Bold())
            c.Text("Italic only.", template.Italic())
            c.Text("Bold and italic.", template.Bold(), template.Italic())
        })
    })

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

Four lines, four visible styles. No WithFont calls at all. The PDF ends up referencing Helvetica, Helvetica-Bold, Helvetica-Oblique, and Helvetica-BoldOblique as non-embedded Type 1 entries, which every PDF viewer already has.

What gpdf actually does

The resolver builds a variant ID from the style flags:

Bold()Italic()Variant ID looked up
nonoHelvetica
yesnoHelvetica-Bold
noyesHelvetica-Italic → aliased to Helvetica-Oblique
yesyesHelvetica-BoldItalic → aliased to Helvetica-BoldOblique

The alias step is the only thing that makes Helvetica special. buildFontVariantID always emits the generic -Italic / -BoldItalic suffixes regardless of family; the Standard 14 init hook then points Helvetica-Italic at Helvetica-Oblique and Helvetica-BoldItalic at Helvetica-BoldOblique so the metrics match what the viewer draws. Courier gets the same treatment. Times doesn't need the alias because its upright form is already called Times-Italic / Times-BoldItalic in the canonical Adobe names.

The trap: TrueType fonts need all four registrations

This is where CJK documents silently break. If you register Noto Sans JP but forget one of the variants, the missing slot does not fall through to Bold or Italic — it falls through straight to the base family.

// Looks fine. Isn't.
doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", regular),
    gpdf.WithFont("NotoSansJP-Bold", bold),
    gpdf.WithDefaultFont("NotoSansJP", 12),
)

// This renders in plain NotoSansJP — not bold, not italic.
c.Text("強調したい", template.Bold(), template.Italic())

The reason is in the resolver. It tries NotoSansJP-BoldItalic first, misses, then falls back to exactly one thing: the base family NotoSansJP. There's no intermediate step that tries the bold version as a consolation prize. You asked for bold-italic, you got regular.

The fix is to register every variant you intend to use:

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 := mustRead("NotoSansJP-Regular.ttf")
    bold := mustRead("NotoSansJP-Bold.ttf")
    italic := mustRead("NotoSansJP-Italic.ttf")
    boldItalic := mustRead("NotoSansJP-BoldItalic.ttf")

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithFont("NotoSansJP", regular),
        gpdf.WithFont("NotoSansJP-Bold", bold),
        gpdf.WithFont("NotoSansJP-Italic", italic),
        gpdf.WithFont("NotoSansJP-BoldItalic", boldItalic),
        gpdf.WithDefaultFont("NotoSansJP", 12),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("通常のテキスト")
            c.Text("強調", template.Bold(), template.Italic())
        })
    })

    data, _ := doc.Generate()
    os.WriteFile("jp-emphasis.pdf", data, 0o644)
}

func mustRead(path string) []byte {
    b, err := os.ReadFile(path)
    if err != nil { log.Fatal(err) }
    return b
}

Noto Sans JP's official distribution doesn't actually ship a slanted italic cut — italic Japanese typography is unusual — so in practice most Japanese documents register only regular and bold and simply don't use template.Italic() on Japanese spans. That's fine. The rule is: if you never call Italic() on a family, you don't need its italic variant. The trap is only when you call Italic() and haven't registered the file.

Mixing bold and italic in the same paragraph

c.Text applies one style to the whole string. For mid-sentence emphasis use c.RichText:

c.RichText(func(rt *template.RichTextBuilder) {
    rt.Span("The ")
    rt.Span("quick brown fox", template.Bold(), template.Italic())
    rt.Span(" jumps over the lazy dog.")
})

Each rt.Span gets its own style flags, and the layout engine line-breaks across spans the way a word processor would. Bold() + Italic() on a single Span hits the same -BoldItalic variant lookup as c.Text — it's the same code path.

One thing worth naming: Bold() and Italic() are commutative. template.Italic(), template.Bold() and template.Bold(), template.Italic() produce identical output. They're setting two different fields (FontWeight and FontStyle) on the same document.Style, so ordering doesn't matter.

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