Todas as publicações

PDFs em japonês com Go: o guia definitivo de 2026

Como gerar PDFs em japonês com Go em 2026 — fontes, subconjuntos TrueType, kanji/kana/ASCII misturados, e por que não precisa de CGO nem Chromium.

por gpdf team

TL;DR

Se seu PDF em Go renderiza こんにちは como cinco caixas de tofu, a correção são duas linhas de configuração, não uma reescrita. Carregue um TTF japonês, passe gpdf.WithFont para NewDocument, escreva em japonês. O gpdf faz o subconjunto da tabela de glifos automaticamente, então a saída carrega apenas os caracteres que você usou — cerca de 30 KB, não os 5 MB da fonte completa. Este guia é o mapa: por que gerar PDFs em japonês com Go tem sido estranhamente difícil, as quatro opções reais em 2026, um exemplo completo e funcional, os detalhes internos do subconjunto de fontes, casos limite de escritas mistas, e o que ainda não funciona.

Por que este guia existe

Tirar um PDF com texto japonês do Go deveria ser trabalho de cinco minutos. Para muitos times é um dia e meio.

A história usual: alguém troca para AddUTF8Font, o PDF mostra retângulos em branco — o infame 豆腐 — e um dev sênior passa a tarde descobrindo se o problema é o caminho da fonte, a flag de subconjunto, o CMap, a flag UTF-8 ou o leitor de PDF. Ao cair da noite há uma thread no Slack chamada "POR QUE 漢字 AINDA ESTÁ QUEBRADO" e um PR que adiciona três funções auxiliares das quais todos já se arrependem.

A causa raiz não é nenhuma delas. A biblioteca de PDF mais longeva do Go foi desenhada em 2002 para PHP e Latin-1, e quase todos os tutoriais japoneses escritos desde então estão brigando com esse legado. Este guia é a versão 2026: o que realmente funciona quando você começa do zero, e o que ainda é genuinamente difícil.

Todo o código deste post roda com gpdf v1.x em 2026-04. Os números de benchmark são de um Apple M1 com Go 1.25.

O problema do tofu em 90 segundos

PDF não se importa com Unicode. Ele se importa com IDs de glifo — índices inteiros para a tabela de glifos embutida da fonte. Quando você escreve "こんにちは" num PDF, alguém tem que:

  1. Parsear o TTF e achar o ID de glifo para cada code point (via a subtabela cmap da fonte).
  2. Escrever um ToUnicode CMap para que o leitor de PDF consiga mapear glifos de volta ao texto quando o usuário copiar ou buscar.
  3. Fazer o subconjunto da fonte para que o PDF não carregue todos os 20.000 glifos da Noto Sans JP.
  4. Embutir o resultado com as tabelas name, OS/2, head e as referências de codificação corretamente costuradas.

Se algum passo falta ou está errado, o leitor não acha glifo para o code point e pinta uma caixa de tofu. Os linhagens arquivadas jung-kurt/gofpdf e go-pdf/fpdf encaixaram tudo isso num modelo interno de fonte de byte único — o FPDF original de 2002 só conhecia Latin-1. É por isso que a configuração é frágil, a saída frequentemente embute a fonte completa em vez do subconjunto, e os modos de falha variam por SO e leitor de PDF.

gpdf trata CJK como caso de primeira classe. O subsetter TTF está no pacote core. O ToUnicode CMap é escrito automaticamente. Não há dança de AddUTF8Font porque não há legado de fonte de byte único para contornar.

As quatro opções reais em 2026

Antes de escrever código: o campo honesto. "Capaz de japonês" significa "renderiza qualquer texto japonês sem crashes ou tofu, dado um TTF correto".

OpçãoLicençaDepsCaminho CJKTamanho para 300 caracteresNotas
go-pdf/fpdf (arquivada 2025)MITstdlibEncaixe AddUTF8Font~5 MB (fonte cheia)Encaixe em core Latin-1. Subconjunto é opt-in e imperfeito.
signintech/gopdfMITstdlibAddTTFFont + manual~3 MB típicoBaixo nível. Você escreve coordenadas. Subconjunto existe mas você conduz.
chromedp + ChromiumMIT + Chromebinário ChromiumNativo via navegadorvariávelHTML/CSS. Precisa de fontes instaladas no container. Imagem 500 MB+.
gpdfMITsó stdlibNativo, subconjunto automático~30 KBGo puro. API builder. ToUnicode CMap escrito por você.

Duas coisas que vale sublinhar:

A diferença de 160× entre "fonte cheia embutida" e "subconjunto automático" não é arredondamento. Uma fatura de e-commerce para o mercado japonês com dez linhas precisa de talvez 120 glifos japoneses únicos. Embutir a Noto Sans JP completa (5,1 MB) em cada fatura significa que sua conta de storage carrega os mesmos 5 MB de dados de glifos 10 milhões de vezes ao fim do ano. O subconjunto carrega só o que você usou.

"chromedp funciona" é verdade e também é a resposta mais cara. Se seu time já roda uma frota de Chrome headless para screenshots, pegar carona dela para PDFs está bem. Se não, subir uma só para imprimir 日本語 é muita infraestrutura para um problema de 40 linhas de Go.

O caminho mais curto que funciona

Comece com isto. É completo — copie, salve como main.go, ponha dois TTFs ao lado, go run main.go.

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, err := os.ReadFile("NotoSansJP-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }
    bold, err := os.ReadFile("NotoSansJP-Bold.ttf")
    if err != nil {
        log.Fatal(err)
    }

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("NotoSansJP", regular),
        gpdf.WithFont("NotoSansJP-Bold", bold),
        gpdf.WithDefaultFont("NotoSansJP", 11),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("請求書", template.FontFamily("NotoSansJP-Bold"), template.FontSize(22))
            c.Text("2026 年 4 月 16 日")
        })
    })
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(7, func(c *template.ColBuilder) {
            c.Text("株式会社 ABC 御中", template.FontSize(13))
            c.Text("〒 100-0001 東京都千代田区千代田 1-1")
        })
        r.Col(5, func(c *template.ColBuilder) {
            c.Text("合計 ¥ 128,000", template.FontFamily("NotoSansJP-Bold"), template.AlignRight())
            c.Text("支払期限: 2026-05-31", template.AlignRight())
        })
    })

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

Coisas que vale notar sem narrar cada uma:

  • Sem AddUTF8Font, sem flag UTF-8, sem argumento de caminho de fonte para Text. gpdf.WithFont registra uma família; c.Text só escreve Unicode. O encanamento fica interno.
  • Negrito é uma família à parte, não uma flag. Isso combina com a forma como os TTFs são distribuídos (Noto Sans JP Regular e Noto Sans JP Bold são arquivos distintos com tabelas name diferentes). As variantes Gothic/Mincho, ou Source Han Sans JP Normal/Heavy, seguem o mesmo padrão.
  • Layout é grid, não cursor. r.Col(7, ...) e r.Col(5, ...) somam 12. As larguras são declarativas; você não calcula coordenadas x. Mais em Como funciona a grid de 12 colunas no gpdf.
  • AlignRight() é agnóstico de locale. O japonês "¥ 128,000" alinha à direita do mesmo jeito que "R$ 1.280,00" alinharia. O conteúdo do texto não muda o código de layout.

Abra o invoice-ja.pdf resultante em qualquer leitor. Selecione "株式会社 ABC 御中". Cole num editor de texto. Você obtém 株式会社 ABC 御中, não uma bagunça. Isso é o ToUnicode CMap trabalhando; gpdf escreve um por padrão.

Subconjunto de fontes: a bomba de tamanho escondida

Aqui vai a propriedade mais importante de CJK-em-PDF que os tutoriais pulam: embutir por subconjunto.

Uma fonte TTF é uma coleção de contornos de glifo mais tabelas de metadados. Noto Sans JP Regular traz cerca de 17.500 glifos e pesa 5,1 MB. Uma fatura típica usa entre 60 e 200 caracteres japoneses únicos. Embutir a fonte completa em cada documento é desperdício de uma ordem de magnitude.

Embutir por subconjunto mantém só os glifos que você usou. gpdf faz isso automaticamente. Você pode ver isso rodando o exemplo acima:

$ ls -l invoice-ja.pdf
-rw-r--r--  1 dev  staff  34892 Apr 16 10:12 invoice-ja.pdf

34 KB. Para comparar, o mesmo documento gerado com go-pdf/fpdf e AddUTF8Font("NotoSansJP", "NotoSansJP-Regular.ttf", true) — onde o terceiro argumento é a flag UTF-8 — dá 4,9 MB. Mesma entrada, mesmo texto de saída, arquivo 143× maior. O motivo é que o caminho de código do fpdf embute a tabela inteira da fonte em vez de fazer subconjunto no momento do emit.

Algumas consequências que vale nomear:

  • A 10 faturas por segundo (escala SaaS normal), a diferença de subconjunto é a diferença entre 0,3 MB/s e 43 MB/s de bytes PDF de saída. Seu load balancer tem opinião sobre isso.
  • Contas de cold storage escalam linearmente com o tamanho do PDF. Cinco milhões de faturas arquivadas a 5 MB cada dão 25 TB. A 30 KB cada, 150 GB. Preço de object storage faz disso uma linha mensal de quatro dígitos contra dois.
  • Entrega por email tem limites de anexo de 10–25 MB dependendo do provedor. Uma fatura japonesa de 5 MB mais qualquer outro anexo mais a codificação MIME começa a bater nesse teto.

gpdf faz o subconjunto em tempo de render. Não há flag para ligar. Você pode ver quais glifos foram parar na saída rodando a ferramenta de verificação do gpdf localmente, mas a versão curta é: se você usou , , e , esses quatro glifos estão na saída e os outros 17.496 não.

Escritas mistas: kanji + kana + ASCII na mesma linha

Texto japonês raramente é só japonês. Uma linha real num documento japonês parece com isto:

API の P95 レイテンシは 50 ms 未満です。

São cinco escritas: romaji (ASCII Latin), katakana, hiragana, kanji (Han) e numerais. Uma implementação ingênua escolhe a fonte errada para as partes ASCII e você fica com um "API" monoespaçado ao lado de japonês proporcional, o que fica horrível.

O comportamento padrão do gpdf é renderizar cada code point com a família registrada. Se Noto Sans JP é seu padrão, API e 50 ms são desenhados com os glifos Latin da Noto Sans JP, que a Noto fornece (a maioria das superfamílias japonesas fornece). O resultado parece uma única tipografia, porque é.

Se você quer misturar famílias deliberadamente — por exemplo, uma sans condensada para ASCII e Noto Sans JP para japonês — registre as duas e sobrescreva por chamada de c.Text:

c.Text("API の P95 レイテンシは 50 ms 未満です。",
    template.FontFamily("NotoSansJP"))
c.Text("API latency (P95) is under 50 ms.",
    template.FontFamily("InterVariable"))

Duas c.Text, duas famílias, zero lógica de detecção de escrita no seu código. Se precisar de mistura intra-linha (Inter para ASCII + Noto para japonês na mesma frase), vem no gpdf v1.2; hoje o workaround é quebrar nas fronteiras de escrita manualmente e dispor numa linha horizontal de colunas.

O que ainda dói

A história do PDF japonês em Go está 95% resolvida. Aqui estão os 5%.

Texto vertical (縦書き) ainda não está lá. gpdf renderiza só texto horizontal em v1.x. A diagramação japonesa tradicional — colunas da direita para a esquerda, caracteres de cima para baixo, com a rotação apropriada de glifos e reposicionamento de pontuação — é uma mudança profunda do motor de layout, não um ajuste de renderização. A issue aberta tem uma proposta de design; vai aterrissar quando aterrissar. Por enquanto, se você precisa de 縦書き para livros ou correspondência formal, gere com uma ferramenta que suporta (Word, InDesign ou um pipeline pandoc + LuaLaTeX) e embuta o PDF resultante com gpdf.Merge.

Anotações ruby (振り仮名) são só workaround. Não há primitiva c.Ruby("漢字", "かんじ"). Se você precisa de ruby para conteúdo infantil ou livros de língua, o workaround é uma coluna de duas linhas: texto kana pequeno em cima, kanji normal embaixo, alinhados. Funciona, mas é manual, e o kerning fino nas fronteiras de furigana exige cuidado.

Fallbacks complexos entre múltiplas fontes CJK. Se um usuário submete texto que mistura kanji japonês com caracteres só chineses (as formas diferem — , , renderizam sutilmente diferentes em CN vs JP), você precisa partir manualmente e usar duas famílias. O gpdf não faz auto-fallback entre famílias dentro de uma única chamada de c.Text. Na prática muito poucos documentos precisam disso; se o seu precisa, veja PDFs multilíngues: misturando JP/CN/KR/EN (B-070 pendente).

Conformidade PDF/A-2b estrita com japonês. gpdf produz saída PDF/A via gpdf.WithPDFA, mas os requisitos estritos sobre metadados de glifos embutidos, o span ActualText em cada run CJK, e árvores de estrutura etiquetadas ainda estão sendo afinados para o caso CJK. Se você está exportando para arquivamento de longo prazo para atender à NFe, ao DANFE ou ao PDF/A-3b brasileiro (ou à 電子帳簿保存法 japonesa), valide com uma ferramenta de terceiros (veraPDF é grátis) antes de committar.

Nenhum desses é bloqueio para os casos comuns: faturas, relatórios, extratos, recibos, certificados. Vale nomear porque alguém lendo isto está prestes a encontrar um em produção, e "está no roadmap" é menos útil que "aqui está o workaround".

Uma palavra sobre conformidade

Um pedaço de contexto de ecossistema que costuma ficar não dito: gerar PDFs japoneses em 2026 não é só problema tipográfico. Duas mudanças regulatórias o empurram para a conversa de conformidade.

O regime 適格請求書 (nota fiscal qualificada) sob a reforma do imposto de consumo exige que as notas incluam campos específicos (número de negócio registrado, alíquota aplicável, detalhamento) e que sejam retidas de forma à prova de adulteração. PDFs são o formato padrão para isso, e "à prova de adulteração" mapeia para assinaturas digitais PDF — PAdES-B-LT no caso estrito.

A 電子帳簿保存法 (lei de armazenamento eletrônico de livros), revisada em 2024, estendeu mandatos de retenção para incluir notas armazenadas em forma eletrônica. PDFs arquivados precisam atender a certos requisitos de integridade. PDF/A-2b ou PDF/A-3b são o formato-alvo de fato. No Brasil, a NFe requer assinatura digital ICP-Brasil e PDF/A-3 para a DANFE arquivada — a mesma classe de requisito.

Ambos os requisitos se apoiam em recursos nativos do PDF — assinaturas, validação de longo prazo, metadados PDF/A embutidos. HTML-para-PDF via navegador headless não atende a nenhum limpamente: a saída PDF do Chromium não é compatível com PDF/A e não consegue embutir assinaturas digitais num único passo. Um stack Go nativo (gpdf + gpdf/signature para PAdES + gpdf.WithPDFA) faz a cadeia toda num pipeline sem sair do processo.

Isto é um sinal para posts futuros em vez de um mergulho profundo — assinatura e PDF/A merecem cada um seu próprio artigo hero (são B-067 e B-068 no backlog). Mas se você está escolhendo um stack de PDF japonês hoje e conformidade está no seu radar, escolha um stack que possa fazer assinaturas e PDF/A nativamente. O imposto de migração de "funciona hoje" para "passa na auditoria" é real.

FAQ

Preciso instalar fontes no servidor ou no container? Não. gpdf lê bytes TTF; não passa pelo cache de fontes do sistema. os.ReadFile("NotoSansJP-Regular.ttf") ou //go:embed NotoSansJP-Regular.ttf funciona igual em macOS, Linux e Windows, dentro de um container distroless, e em AWS Lambda. Sem fontconfig, sem fc-cache -fv. Essa é uma das razões do gpdf funcionar em imagens FROM scratch.

Noto Sans JP vs Source Han Sans JP — faz diferença? São a mesma família de fonte sob dois nomes. A Adobe publica Source Han Sans JP; o Google reempacota como Noto Sans JP. Cobertura de glifos idêntica. Escolha a distribuição de licença que passar na sua revisão jurídica; ambas são SIL Open Font License. Para documentação neutra de marca usamos Noto Sans JP por padrão porque os nomes de arquivo são mais fáceis de lembrar.

E 游ゴシック (Yu Gothic) ou Hiragino? Fontes proprietárias que vêm com o SO. Você pode usá-las se seu alvo de deploy tem licença (Windows Server traz Yu Gothic; macOS traz Hiragino), mas vai precisar pegar o arquivo TTF e confirmar os termos de redistribuição para seu build de container. Para deploys abertos, fique com Noto Sans JP ou IPAex Gothic (ambas de redistribuição livre).

O PDF renderiza mas Ctrl+F não acha nada. Por quê? Quase sempre é problema de ToUnicode CMap. gpdf escreve um automaticamente, então se você vê isso com gpdf, abra uma issue com o nome do leitor. Se vê com gofpdf, o conserto é ligar a flag UTF-8 e garantir que o leitor suporta fontes CID — versões antigas do Preview.app no macOS têm issues conhecidos. Teste com Adobe Reader ou Chrome como controle.

Como adiciono um caractere JIS X 0213 que não está na fonte? Não tem como — não há glifo para desenhar. A resposta prática é "use uma fonte que cubra JIS X 0213". Noto Sans JP cobre o BMP completo mais JIS X 0213 Nível 1. Para variantes históricas raras, Hanazono Mincho (花園明朝) é o fallback final. Se um code point não está em nenhuma fonte, gpdf emite o caractere de substituição Unicode (U+FFFD) em vez de um tofu silencioso — então você vê na saída e sabe que precisa investigar.

Tem custo de performance entre CJK e ASCII? Pequeno. O benchmark do gpdf para "fatura CJK complexa" é 133 µs por documento num Apple M1, vs 108 µs para uma tabela ASCII 4×10. São ~23% de overhead, quase todo do trabalho maior de lookup de glifo e subconjunto. Para referência, go-pdf/fpdf no mesmo benchmark CJK é 254 µs, e Maroto v2 é 10,4 ms. Renderização japonesa não é o gargalo no seu serviço.

Experimente o gpdf

gpdf é uma biblioteca Go para gerar PDFs. MIT, zero dependências externas, CJK nativo.

go get github.com/gpdf-dev/gpdf

⭐ Star no GitHub · Leia a documentação

Próximas leituras