Todas as publicações

Como usar Noto Sans JP com gpdf?

Registre o TTF static NotoSansJP-Regular com gpdf.WithFont. Não use a variable font. O gpdf faz subset dos 17.000 glifos para menos de 40 KB por PDF.

por gpdf team

A pergunta, de outro jeito

Você quer renderizar texto em japonês num documento do gpdf e escolheu Noto Sans JP — a sans-serif gratuita da Google sob licença SIL OFL que cobre toda a faixa JIS. Você já baixou o zip do Google Fonts. O que falta esclarecer são três coisas: qual arquivo escolher, quais pesos registrar e qual é a única pegadinha escondida dentro do zip.

TL;DR

Use o TTF static NotoSansJP-Regular.ttf que está dentro da pasta static/ do zip. Não use a variable font na raiz. Passe para gpdf.WithFont("NotoSansJP", bytes) e defina como fonte padrão. O gpdf faz subset dos ~17.000 glifos e mantém apenas os que você renderizou de fato — uma fatura típica fica com 20–40 KB de dados de fonte no PDF final.

Exemplo completo

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)
    }
}

Baixe o zip de Noto Sans JP no Google Fonts, extraia static/NotoSansJP-Regular.ttf, coloque ao lado de main.go e rode go run main.go.

Pegue o TTF static, não a variable font

Na página do Google Fonts, clique em Get font → Download all e descompacte. Dentro do zip há dois grupos que parecem equivalentes e não são:

  • NotoSansJP-VariableFont_wght.ttf na raiz — a variable font, ~7 MB, carrega todos os pesos de 100 a 900 num único arquivo
  • static/ — nove TTFs separados, de NotoSansJP-Thin.ttf a NotoSansJP-Black.ttf, cada um com ~5 MB

Use os de static/.

O parser TrueType do gpdf foi escopado de propósito. Ele lida com contornos de glifos, glifos compostos, cmap e hmtx — as tabelas necessárias para renderizar texto com peso fixo. Mas ele não avalia fvar, gvar nem HVAR, que são as tabelas OpenType responsáveis por tornar uma variable font realmente variável. Se você passar o VariableFont_wght.ttf, uma de duas: ou o parser dá erro de forma limpa, ou — pior — ele pega os glifos da instância padrão e ignora em silêncio qualquer eixo de peso que você achava estar ajustando.

A matemática do tamanho também joga contra a variable. Ela carrega os contornos de todos os pesos no mesmo arquivo — esse é o propósito do formato. Se você usa só Regular, está pagando por oito pesos que nunca aparecem no PDF. Static Regular tem 5 MB; a variable tem 7 MB. O subsetting reduz os dois, mas o static é uma entrada mais limpa.

As quatro linhas que importam

Tudo que interessa está nas opções do construtor:

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

O nome da família ("NotoSansJP") é arbitrário. O gpdf usa só como chave de lookup — não é caminho de arquivo, nem campo lido dos metadados da fonte. Chame de "body", "jp" ou "Noto" se ficar mais natural no código. Só mantenha consistente com o argumento que você passar depois em template.FontFamily(...).

WithDefaultFont é o que te poupa de escrever template.FontFamily("NotoSansJP") em cada c.Text. Sem ele, o gpdf cai em Helvetica — que não cobre nenhum codepoint CJK — e você acaba com caixinhas vazias (tofu, □□□) em todo o texto sem família explícita. Uma hora perdida descobrindo por que "só os títulos saem certos".

Quais pesos você realmente precisa?

A maioria de faturas, recibos e relatórios usa dois: Regular e Bold. Registre os dois:

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

Com o sufixo -Bold, template.Bold() pega automaticamente. Mesma regra para -Italic e -BoldItalic, só que Noto Sans JP não vem com itálico — fontes CJK geralmente não publicam variante oblíqua porque o desenho dos glifos não tem inclinação natural. Se o layout pede ênfase itálica num trecho em japonês, use cor, tamanho ou peso no lugar.

Folhetos de marketing às vezes pedem Medium ou SemiBold para citações em destaque. Tudo bem. Registre com qualquer sufixo e referencie pelo nome da família diretamente:

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

O atalho Bold/Italic por sufixo só se conecta com os nomes literais -Bold / -Italic / -BoldItalic. Qualquer outra coisa vai por nome de família.

O tamanho depois do subsetting

Noto Sans JP Regular tem ~5 MB em disco. Esse número empurra alguns times a montar CDN separado só para fonte, ou a adicionar pós-processamento para retirar a fonte do PDF. Com o gpdf, nenhum dos dois é necessário.

O que de fato cai no PDF:

DocumentoGlifos usadosDados de fonte no PDF
Recibo de uma linha (~15 caract.)~14~11 KB
Fatura típica (~200 caract.)~80~28 KB
Relatório de 10 páginas (~8.000 caract.)~900~180 KB
Dump tipo dicionário (JIS Nível 1 completo)~6.800~2,1 MB

(gpdf v1.0, subsetting estático ligado. Os números oscilam alguns KB conforme onde os IDs de glifo caem em CFF e hmtx.)

Para uma fatura final de 50 KB, mais da metade é dado de fonte. Mesmo assim é uma fração do que seriam 5 MB incorporados sem subsetting, e o visualizador abre o PDF instantaneamente.

Noto Sans JP vs. Noto Sans CJK JP — não confunda

Existem duas famílias Noto que dizem lidar com japonês e os nomes fazem parecer intercambiáveis. Não são.

Noto Sans JP é a que você quer. Distribuída como TTF, um único idioma, um arquivo por peso. É o que sai do Google Fonts.

Noto Sans CJK JP é a super-família pan-CJK. Distribuída como OpenType Collection (.ttc), um único arquivo que contém japonês, chinês simplificado, chinês tradicional e coreano unificados num pacote só. É o que vinha nas primeiras releases do Noto e o que você encontra em notofonts.github.io/noto-cjk.

O gpdf suporta TTF direto. TTC é um formato container — você precisaria escolher o índice de face correto antes de entregar os bytes ao WithFont, e o cmap dentro de cada face é calibrado para uma locale CJK específica, o que significa estar tomando decisões implícitas sobre unificação Han. É mais claro tomá-las de forma explícita, escolhendo o TTF específico de JP.

Começando hoje? Use Noto Sans JP. Já tem NotoSansCJK-Regular.ttc num projeto legado? Extraia a face JP com pyftsubset ou fonttools e faça commit do TTF resultante como artefato canônico do repositório.

Empacotando a fonte no binário

Geradores de PDF costumam rodar em contêineres, e a forma mais limpa de distribuir a fonte é compilá-la junto:

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),
    )
    // ...
}

O binário cresce de ~8 MB para ~13 MB. Em troca, a imagem Docker tem um artefato só em vez de dois, COPY --from=builder /app /app basta, e ninguém consegue subir um contêiner quebrado por ter esquecido o arquivo de fonte. Para um batch que gera milhares de PDFs por dia, esse é o default certo.

Leitura relacionada

Experimente o gpdf

O gpdf é uma biblioteca Go para gerar PDFs. Licença MIT, zero dependências externas, suporte nativo a CJK.

go get github.com/gpdf-dev/gpdf

⭐ Star no GitHub · Ler a documentação